# Fichier: python_cheats/cheatsheets/microservices.txt
# Cheatsheet Microservices Python - Guide Complet Architecture & Implémentation


[OK] INTRODUCTION AUX MICROSERVICES

# ============================================================================
# QU'EST-CE QUE LES MICROSERVICES ?
# ============================================================================

# ANALOGIE SIMPLE:
# Imaginez une entreprise:
# 
# MONOLITHE = Entreprise avec un seul bâtiment où tout le monde travaille
# - Marketing, Ventes, Comptabilité, IT, RH dans le même open space
# - Si l'électricité coupe, tout s'arrête
# - Pour agrandir Marketing, on doit déménager tout le monde
#
# MICROSERVICES = Entreprise avec plusieurs bâtiments spécialisés
# - Marketing dans bâtiment A, Ventes dans B, Comptabilité dans C
# - Si Marketing a une panne, les autres continuent
# - Pour agrandir Marketing, on agrandit uniquement le bâtiment A
# - Chaque équipe peut choisir ses outils (Windows, Mac, Linux)

# DÉFINITION TECHNIQUE:
# Architecture où l'application est divisée en services indépendants
# Chaque service = une fonctionnalité métier spécifique
# Les services communiquent entre eux via des APIs

# EXEMPLE CONCRET - Site E-commerce:
#
# ARCHITECTURE MONOLITHE (tout dans une seule application):
# ┌────────────────────────────────────────┐
# │   APPLICATION MONOLITHE                │
# │  ┌──────────────────────────────────┐  │
# │  │  Code Utilisateurs               │  │
# │  │  Code Produits                   │  │
# │  │  Code Commandes                  │  │
# │  │  Code Paiement                   │  │
# │  │  Code Notifications              │  │
# │  └──────────────────────────────────┘  │
# │           v                            │
# │  ┌──────────────────────────────────┐  │
# │  │    BASE DE DONNÉES UNIQUE        │  │
# │  └──────────────────────────────────┘  │
# └────────────────────────────────────────┘
#
# ARCHITECTURE MICROSERVICES (services séparés):
# ┌─────────────┐  ┌─────────────┐  ┌─────────────┐
# │  Service    │  │  Service    │  │  Service    │
# │ Utilisateurs│  │  Produits   │  │  Commandes  │
# │    v        │  │    v        │  │    v        │
# │   DB User   │  │ DB Products │  │  DB Orders  │
# └─────────────┘  └─────────────┘  └─────────────┘
#        ^v              ^v                  ^v
#        └──────── Communication via APIs ──────┘

# AVANTAGES EXPLIQUÉS:

# [OK] Scalabilité indépendante par service
#   Exemple: Black Friday -> beaucoup de commandes
#   Monolithe: Dupliquer TOUTE l'application (gaspillage)
#   Microservices: Dupliquer SEULEMENT le service Commandes

# [OK] Déploiement indépendant
#   Exemple: Bug dans Notifications
#   Monolithe: Redéployer toute l'app (risque sur tout)
#   Microservices: Corriger et redéployer SEULEMENT Notifications

# [OK] Technologies différentes par service
#   Exemple: 
#   - Service Utilisateurs en Python (FastAPI)
#   - Service Recherche en Java (Elasticsearch)
#   - Service ML en Python (TensorFlow)
#   Chaque équipe choisit le meilleur outil pour son besoin

# [OK] Isolation des pannes
#   Exemple: Service Recommandations plante
#   Monolithe: Tout le site tombe
#   Microservices: Seules les recommandations ne marchent pas,
#                  le reste du site fonctionne normalement

# [OK] Équipes autonomes
#   Chaque équipe possède son service
#   Pas besoin d'attendre les autres pour déployer

# [OK] Maintenance facilitée
#   Code plus petit = plus facile à comprendre
#   Un service = 1000 lignes vs Monolithe = 100 000 lignes

# [OK] Cycle de développement rapide
#   Équipe Produits développe en parallèle d'équipe Commandes
#   Pas de conflits de code (git merge hell)

# INCONVÉNIENTS EXPLIQUÉS:

# [X] Complexité architecture
#   Au lieu de 1 application, vous en gérez 10+
#   Plus de configs, plus de surveillance

# [X] Gestion données distribuées
#   Exemple: Créer commande = vérifier stock + débiter client + créer livraison
#   Monolithe: Transaction SQL simple (tout ou rien)
#   Microservices: Orchestrer 3 services (plus complexe)

# [X] Latence réseau
#   Monolithe: Appel fonction = 0.001ms
#   Microservices: Appel HTTP = 50-100ms (50 000x plus lent!)

# [X] Debugging difficile
#   Bug: "La commande n'a pas été créée"
#   Monolithe: Logs dans 1 fichier
#   Microservices: Chercher dans logs de 5 services différents

# [X] Tests d'intégration complexes
#   Tester "créer commande" nécessite:
#   Users, Products, Orders, Inventory, Payment services actifs

# [X] Monitoring nécessaire
#   Surveiller 1 app vs 15 services (CPU, RAM, erreurs...)

# [X] DevOps obligatoire
#   Besoin Docker, Kubernetes, CI/CD pipelines

# QUAND UTILISER LES MICROSERVICES?

# [OK] Application complexe et large
#   Exemple: Amazon, Netflix (milliers de fonctionnalités)
#   NON pour: Blog personnel, site vitrine

# [OK] Équipes multiples (5+ équipes)
#   Chaque équipe travaille indépendamment
#   NON pour: 2-3 développeurs

# [OK] Besoin de scalabilité différenciée
#   Exemple: Service Vidéo consomme beaucoup, Service Profil non
#   NON pour: Charge uniforme sur toute l'app

# [OK] Cycles de déploiement rapides
#   Déployer nouvelles features plusieurs fois par jour
#   NON pour: Déploiement mensuel

# [OK] Technologies variées nécessaires
#   Exemple: IA (Python), Temps réel (Node.js), Admin (Java)
#   NON pour: Tout en Python suffit

# QUAND ÉVITER LES MICROSERVICES?

# [X] Petit projet / MVP (Minimum Viable Product)
#   Commencer simple, migrer plus tard si nécessaire

# [X] Équipe réduite (1-4 personnes)
#   Overhead trop important pour petite équipe

# [X] Monolithe fonctionne bien
#   "If it ain't broken, don't fix it"

# [X] Pas d'expertise DevOps
#   Microservices = besoin compétences infrastructure

# [X] Latence critique
#   Trading haute fréquence, jeux temps réel
#   Chaque milliseconde compte

# RÈGLE D'OR POUR DÉBUTANTS:
# "Commencez avec un monolithe bien structuré.
#  Migrez vers microservices SEULEMENT quand vous avez:
#  - Un vrai problème de scalabilité
#  - Plusieurs équipes
#  - Budget DevOps"


[OK] FRAMEWORKS PYTHON POUR MICROSERVICES

# ============================================================================
# FASTAPI - LE FRAMEWORK MODERNE (RECOMMANDÉ POUR DÉBUTANTS)
# ============================================================================

# POURQUOI FASTAPI ?
# - Moderne (2018)
# - Très rapide (performances comparables à Node.js)
# - Documentation automatique (Swagger UI)
# - Validation automatique des données
# - Support async/await natif
# - Facile à apprendre

# Installation
pip install fastapi
pip install "uvicorn[standard]"  # Serveur ASGI

# ============================================================================
# VOTRE PREMIER MICROSERVICE - ÉTAPE PAR ÉTAPE
# ============================================================================

# Fichier: main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import Optional, List
import uvicorn

# ÉTAPE 1: Créer l'application FastAPI
# C'est comme créer votre "restaurant" qui va servir des requêtes
app = FastAPI(
    title="Users Service",      # Nom du service
    version="1.0.0",            # Version
    description="Service de gestion des utilisateurs"
)

# ÉTAPE 2: Définir le modèle de données avec Pydantic
# Pydantic = validation automatique des données
# Comme un "formulaire" avec des règles de validation
class User(BaseModel):
    id: Optional[int] = None        # Optionnel, généré automatiquement
    name: str                        # Obligatoire, doit être un texte
    email: str                       # Obligatoire, doit être un texte
    active: bool = True              # Optionnel, True par défaut
    
    # Exemple d'instance valide:
    # {"name": "John", "email": "john@example.com", "active": true}
    
    # Exemple invalide (FastAPI rejettera automatiquement):
    # {"name": 123, "email": "invalid"}  # name doit être string

# ÉTAPE 3: Base de données simulée (en mémoire)
# En production, vous utiliseriez PostgreSQL, MongoDB, etc.
# Pour apprendre, un dictionnaire Python suffit
users_db = {}  # Stocke les users: {1: User(...), 2: User(...)}

# ============================================================================
# ENDPOINTS (ROUTES) - Les "portes d'entrée" de votre service
# ============================================================================

# ENDPOINT 1: Page d'accueil (GET /)
# GET = Récupérer des informations (comme ouvrir un livre pour lire)
@app.get("/")
async def root():
    """
    Endpoint le plus simple
    URL: http://localhost:8001/
    Méthode: GET
    Retourne: Un simple message JSON
    """
    return {"service": "users", "status": "running"}
# Test dans navigateur: http://localhost:8001/
# Résultat: {"service": "users", "status": "running"}

# ENDPOINT 2: Health check (vérifier si le service est vivant)
@app.get("/health")
async def health_check():
    """
    Endpoint de santé - utilisé par Docker, Kubernetes
    pour vérifier si le service fonctionne
    """
    return {"status": "healthy"}
# Test: curl http://localhost:8001/health
# Résultat: {"status": "healthy"}

# ENDPOINT 3: Créer un utilisateur (POST /users)
# POST = Créer une nouvelle ressource (comme remplir un formulaire)
@app.post("/users", response_model=User, status_code=201)
async def create_user(user: User):
    """
    Créer un nouvel utilisateur
    
    URL: http://localhost:8001/users
    Méthode: POST
    Body: {"name": "John Doe", "email": "john@example.com"}
    
    Paramètre 'user: User':
    - FastAPI lit automatiquement le JSON du body
    - Valide que le JSON correspond au modèle User
    - Si invalide, retourne erreur 422 automatiquement
    
    response_model=User:
    - Spécifie le format de la réponse
    - Génère automatiquement la documentation
    
    status_code=201:
    - 201 = Created (standard REST)
    - 200 = OK (par défaut)
    """
    # Générer ID automatiquement
    user.id = len(users_db) + 1
    
    # Stocker dans notre "base de données"
    users_db[user.id] = user
    
    # Retourner l'utilisateur créé (avec son ID)
    return user

# Test avec curl:
# curl -X POST http://localhost:8001/users \
#   -H "Content-Type: application/json" \
#   -d '{"name": "John Doe", "email": "john@example.com"}'
# 
# Résultat: {"id": 1, "name": "John Doe", "email": "john@example.com", "active": true}

# ENDPOINT 4: Lister tous les utilisateurs (GET /users)
@app.get("/users", response_model=List[User])
async def list_users():
    """
    Récupérer la liste de tous les utilisateurs
    
    URL: http://localhost:8001/users
    Méthode: GET
    
    List[User] = Liste d'objets User
    """
    # Convertir le dictionnaire en liste
    return list(users_db.values())

# Test: curl http://localhost:8001/users
# Résultat: [{"id": 1, "name": "John", ...}, {"id": 2, "name": "Jane", ...}]

# ENDPOINT 5: Récupérer un utilisateur spécifique (GET /users/{user_id})
@app.get("/users/{user_id}", response_model=User)
async def get_user(user_id: int):
    """
    Récupérer un utilisateur par son ID
    
    URL: http://localhost:8001/users/1
    Méthode: GET
    
    {user_id} = Path parameter (paramètre dans l'URL)
    FastAPI le convertit automatiquement en int
    
    Si vous passez /users/abc, FastAPI retourne erreur 422
    car "abc" n'est pas un int
    """
    # Vérifier si l'utilisateur existe
    if user_id not in users_db:
        # HTTPException = retourner une erreur HTTP
        raise HTTPException(
            status_code=404,                    # 404 = Not Found
            detail="User not found"             # Message d'erreur
        )
    
    return users_db[user_id]

# Test existant: curl http://localhost:8001/users/1
# Résultat: {"id": 1, "name": "John Doe", ...}
#
# Test inexistant: curl http://localhost:8001/users/999
# Résultat: {"detail": "User not found"}  (erreur 404)

# ENDPOINT 6: Mettre à jour un utilisateur (PUT /users/{user_id})
# PUT = Mettre à jour une ressource existante
@app.put("/users/{user_id}", response_model=User)
async def update_user(user_id: int, user: User):
    """
    Mettre à jour un utilisateur existant
    
    URL: http://localhost:8001/users/1
    Méthode: PUT
    Body: {"name": "John Updated", "email": "john.new@example.com"}
    
    Deux paramètres:
    - user_id: int -> vient de l'URL (path parameter)
    - user: User -> vient du body (body parameter)
    """
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="User not found")
    
    # Forcer l'ID (éviter qu'on change l'ID via le body)
    user.id = user_id
    
    # Mettre à jour dans la DB
    users_db[user_id] = user
    
    return user

# Test:
# curl -X PUT http://localhost:8001/users/1 \
#   -H "Content-Type: application/json" \
#   -d '{"name": "John Updated", "email": "john.new@example.com"}'

# ENDPOINT 7: Supprimer un utilisateur (DELETE /users/{user_id})
# DELETE = Supprimer une ressource
@app.delete("/users/{user_id}", status_code=204)
async def delete_user(user_id: int):
    """
    Supprimer un utilisateur
    
    URL: http://localhost:8001/users/1
    Méthode: DELETE
    
    status_code=204:
    - 204 = No Content (suppression réussie, pas de body)
    - Standard REST pour DELETE
    """
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="User not found")
    
    # Supprimer de la DB
    del users_db[user_id]
    
    # Retourner None = pas de contenu (204)
    return None

# Test: curl -X DELETE http://localhost:8001/users/1
# Résultat: Pas de contenu, juste status 204

# ============================================================================
# LANCER LE SERVICE
# ============================================================================

if __name__ == "__main__":
    # uvicorn = serveur ASGI (comme gunicorn mais pour async)
    uvicorn.run(
        app,                    # Notre application FastAPI
        host="0.0.0.0",        # Écouter sur toutes les interfaces
        port=8001,             # Port du service
        reload=True            # Auto-reload si code change (dev mode)
    )

# ============================================================================
# COMMENT LANCER
# ============================================================================

# Méthode 1: Directement avec Python
# python main.py

# Méthode 2: Avec uvicorn en ligne de commande (recommandé)
# uvicorn main:app --reload --port 8001
#
# Explications:
# - main:app -> fichier main.py, variable app
# - --reload -> redémarre automatiquement si le code change
# - --port 8001 -> écoute sur le port 8001

# ============================================================================
# TESTER VOTRE SERVICE
# ============================================================================

# Option 1: Documentation automatique (MEILLEUR POUR DÉBUTANTS)
# FastAPI génère automatiquement une interface web interactive!
# Ouvrez dans votre navigateur: http://localhost:8001/docs
# 
# Vous verrez:
# - Liste de tous vos endpoints
# - Formulaires pour tester chaque endpoint
# - Documentation des paramètres
# - Exemples de requêtes/réponses
#
# Cliquez sur "Try it out" pour tester directement!

# Option 2: Documentation alternative (ReDoc)
# http://localhost:8001/redoc
# Plus jolie, mais moins interactive

# Option 3: Avec curl (ligne de commande)
# curl http://localhost:8001/users

# Option 4: Avec Postman (application graphique)
# - Télécharger Postman
# - Créer requête GET http://localhost:8001/users

# Option 5: Avec Python requests
# import requests
# response = requests.get("http://localhost:8001/users")
# print(response.json())

# ============================================================================
# CODES HTTP - CE QUE SIGNIFIENT LES NOMBRES
# ============================================================================

# 2xx = Succès
# 200 OK - Requête réussie
# 201 Created - Ressource créée avec succès
# 204 No Content - Requête réussie, pas de contenu à retourner

# 4xx = Erreur client (votre faute)
# 400 Bad Request - Requête invalide (mauvais JSON)
# 401 Unauthorized - Pas authentifié (login requis)
# 403 Forbidden - Authentifié mais pas autorisé
# 404 Not Found - Ressource inexistante
# 422 Unprocessable Entity - Données invalides (FastAPI l'utilise)

# 5xx = Erreur serveur (leur faute)
# 500 Internal Server Error - Erreur dans le code serveur
# 503 Service Unavailable - Service temporairement indisponible

# ============================================================================
# VALIDATION AUTOMATIQUE - MAGIE DE PYDANTIC
# ============================================================================

# Pydantic valide automatiquement:

# Exemple 1: Type incorrect
# Requête: {"name": 123, "email": "test@example.com"}
# FastAPI rejette automatiquement (name doit être string)
# Retourne 422 avec message d'erreur détaillé

# Exemple 2: Champ manquant
# Requête: {"name": "John"}  # email manquant
# FastAPI rejette (email est obligatoire)

# Exemple 3: Validation avancée
from pydantic import EmailStr, validator

class UserAdvanced(BaseModel):
    name: str
    email: EmailStr  # Valide le format email automatiquement!
    age: Optional[int] = None
    
    @validator('age')
    def age_must_be_positive(cls, v):
        """Validator personnalisé"""
        if v is not None and v < 0:
            raise ValueError('age must be positive')
        return v

# Maintenant:
# {"name": "John", "email": "invalid-email"} -> Rejeté (email invalide)
# {"name": "John", "email": "test@example.com", "age": -5} -> Rejeté (age négatif)

# ============================================================================
# QUERY PARAMETERS (Paramètres dans l'URL)
# ============================================================================

@app.get("/users/search")
async def search_users(
    name: Optional[str] = None,      # ?name=John
    active: bool = True,             # ?active=false
    limit: int = 10                  # ?limit=20
):
    """
    Rechercher utilisateurs avec filtres
    
    URL exemples:
    - /users/search?name=John
    - /users/search?name=John&active=false
    - /users/search?limit=5
    - /users/search (tous les paramètres optionnels)
    
    FastAPI parse automatiquement les query params!
    """
    results = list(users_db.values())
    
    # Filtrer par nom si fourni
    if name:
        results = [u for u in results if name.lower() in u.name.lower()]
    
    # Filtrer par statut
    results = [u for u in results if u.active == active]
    
    # Limiter résultats
    return results[:limit]

# Test: curl "http://localhost:8001/users/search?name=John&limit=5"

# ============================================================================
# ASYNC vs SYNC - QUAND UTILISER async/await?
# ============================================================================

# SYNC (sans async):
@app.get("/sync-example")
def sync_endpoint():
    """Utilisez SYNC quand:
    - Opérations rapides (calculs simples)
    - Pas d'I/O (base de données, réseau, fichiers)
    """
    result = 2 + 2
    return {"result": result}

# ASYNC (avec async):
@app.get("/async-example")
async def async_endpoint():
    """Utilisez ASYNC quand:
    - Appels base de données (await db.query())
    - Appels HTTP externes (await httpx.get())
    - Opérations I/O (await file.read())
    
    Avantage: Pendant l'attente (await), FastAPI peut
    traiter d'autres requêtes = meilleure performance!
    """
    # Simuler opération I/O
    import asyncio
    await asyncio.sleep(0.1)  # Attend 0.1s
    return {"result": "done"}

# RÈGLE SIMPLE:
# - Si vous faites await quelque part -> utilisez async def
# - Si pas de await -> utilisez def (sync)

# ============================================================================
# RÉSUMÉ POUR DÉBUTANTS
# ============================================================================

# 1. FastAPI crée des "endpoints" (URLs) qui répondent aux requêtes HTTP
# 2. Chaque endpoint a une méthode HTTP (GET, POST, PUT, DELETE)
# 3. Pydantic valide automatiquement les données
# 4. FastAPI génère la documentation automatiquement
# 5. Utilisez async/await pour les opérations I/O
# 6. Testez avec /docs (super pratique!)

# PROCHAINES ÉTAPES:
# - Ajouter une vraie base de données (PostgreSQL)
# - Connecter plusieurs services ensemble
# - Ajouter authentification JWT
# - Déployer avec Docker

# ============================================================================
# FLASK - FRAMEWORK CLASSIQUE (ALTERNATIVE)
# ============================================================================

# Flask est plus ancien que FastAPI, mais toujours populaire
# Avantages: Simple, beaucoup de tutoriels, extensions nombreuses
# Inconvénients: Pas de validation auto, pas d'async natif


# === FLASK (CLASSIQUE & FLEXIBLE) ===
pip install flask flask-restful flask-cors

# app.py
from flask import Flask, jsonify, request
from flask_restful import Api, Resource
from flask_cors import CORS

app = Flask(__name__)
CORS(app)
api = Api(app)

users_db = {}

class UserList(Resource):
    def get(self):
        return list(users_db.values()), 200
    
    def post(self):
        data = request.get_json()
        user_id = len(users_db) + 1
        users_db[user_id] = {"id": user_id, **data}
        return users_db[user_id], 201

class UserDetail(Resource):
    def get(self, user_id):
        if user_id not in users_db:
            return {"error": "User not found"}, 404
        return users_db[user_id], 200
    
    def put(self, user_id):
        if user_id not in users_db:
            return {"error": "User not found"}, 404
        data = request.get_json()
        users_db[user_id].update(data)
        return users_db[user_id], 200
    
    def delete(self, user_id):
        if user_id not in users_db:
            return {"error": "User not found"}, 404
        del users_db[user_id]
        return '', 204

api.add_resource(UserList, '/users')
api.add_resource(UserDetail, '/users/<int:user_id>')

@app.route('/health')
def health():
    return jsonify({"status": "healthy"}), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=8001, debug=True)


# === DJANGO REST FRAMEWORK ===
pip install django djangorestframework

# settings.py (extrait)
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'rest_framework',
    'users',
]

REST_FRAMEWORK = {
    'DEFAULT_PAGINATION_CLASS': 'rest_framework.pagination.PageNumberPagination',
    'PAGE_SIZE': 10
}

# users/models.py
from django.db import models

class User(models.Model):
    name = models.CharField(max_length=100)
    email = models.EmailField(unique=True)
    active = models.BooleanField(default=True)
    created_at = models.DateTimeField(auto_now_add=True)

# users/serializers.py
from rest_framework import serializers
from .models import User

class UserSerializer(serializers.ModelSerializer):
    class Meta:
        model = User
        fields = '__all__'

# users/views.py
from rest_framework import viewsets
from .models import User
from .serializers import UserSerializer

class UserViewSet(viewsets.ModelViewSet):
    queryset = User.objects.all()
    serializer_class = UserSerializer

# urls.py
from django.urls import path, include
from rest_framework.routers import DefaultRouter
from users.views import UserViewSet

router = DefaultRouter()
router.register(r'users', UserViewSet)

urlpatterns = [
    path('api/', include(router.urls)),
]


# === NAMEKO (MICROSERVICES NATIF) ===
pip install nameko

# service.py
from nameko.rpc import rpc
from nameko.web.handlers import http

class UsersService:
    name = "users"
    
    users_db = {}
    
    @rpc
    def create_user(self, name, email):
        user_id = len(self.users_db) + 1
        user = {"id": user_id, "name": name, "email": email}
        self.users_db[user_id] = user
        return user
    
    @rpc
    def get_user(self, user_id):
        return self.users_db.get(user_id)
    
    @http('GET', '/users')
    def list_users(self, request):
        return 200, list(self.users_db.values())

# Lancer
nameko run service --broker amqp://guest:guest@localhost



[OK] PATTERNS DE COMMUNICATION

# ============================================================================
# COMMENT LES MICROSERVICES SE PARLENT ENTRE EUX
# ============================================================================

# ANALOGIE:
# Imaginez des collègues dans différents bureaux qui doivent collaborer
#
# Méthode 1: TÉLÉPHONE (Synchrone - REST API)
# - Je t'appelle maintenant
# - J'attends ta réponse
# - Si tu ne réponds pas, je suis bloqué
#
# Méthode 2: EMAIL (Asynchrone - Message Queue)
# - Je t'envoie un email
# - Je continue mon travail
# - Tu réponds quand tu peux
# - Pas bloqué si tu es occupé

# ============================================================================
# MÉTHODE 1: REST API (SYNCHRONE) - LA PLUS SIMPLE
# ============================================================================

# REST = Representational State Transfer
# C'est du HTTP classique (comme quand vous naviguez sur le web)

# SCÉNARIO CONCRET:
# Service Orders doit vérifier si un utilisateur existe avant de créer une commande
# Orders Service -> appelle -> Users Service

# ============================================================================
# CRÉER UN CLIENT HTTP (Pour appeler d'autres services)
# ============================================================================

import requests  # Bibliothèque HTTP simple et populaire

class UserServiceClient:
    """
    Client pour communiquer avec le service Users
    
    C'est comme un "téléphone" pour appeler le service Users
    """
    
    def __init__(self, base_url="http://localhost:8001"):
        """
        base_url: L'adresse du service Users
        - En développement: http://localhost:8001
        - En production: http://users-service:8001 (Docker)
        - En production cloud: https://users.example.com
        """
        self.base_url = base_url
    
    def get_user(self, user_id: int):
        """
        Récupérer un utilisateur par son ID
        
        C'est comme appeler le service Users et demander:
        "Hey, donne-moi les infos de l'utilisateur #123"
        
        Étapes:
        1. Construire l'URL: http://localhost:8001/users/123
        2. Faire une requête HTTP GET
        3. Vérifier si ça a marché (raise_for_status)
        4. Convertir la réponse JSON en dictionnaire Python
        """
        # Construire l'URL complète
        url = f"{self.base_url}/users/{user_id}"
        
        # Faire la requête HTTP GET
        response = requests.get(url)
        
        # Vérifier le status code
        # Si 404, 500, etc. -> lève une exception
        response.raise_for_status()
        
        # Convertir JSON -> dict Python
        return response.json()
        # Exemple retour: {"id": 123, "name": "John", "email": "john@example.com"}
    
    def create_user(self, name: str, email: str):
        """
        Créer un nouvel utilisateur
        
        Étapes:
        1. Préparer les données (dictionnaire Python)
        2. Convertir en JSON automatiquement (requests le fait)
        3. Envoyer requête POST
        4. Retourner l'utilisateur créé
        """
        url = f"{self.base_url}/users"
        
        # Données à envoyer
        data = {
            "name": name,
            "email": email
        }
        
        # POST = créer une ressource
        # json=data -> requests convertit dict en JSON automatiquement
        response = requests.post(url, json=data)
        
        response.raise_for_status()
        return response.json()
    
    def user_exists(self, user_id: int) -> bool:
        """
        Vérifier si un utilisateur existe
        
        Au lieu de laisser l'exception se propager,
        on retourne simplement True/False
        """
        try:
            self.get_user(user_id)
            return True  # Si pas d'exception = utilisateur existe
        except requests.HTTPError as e:
            if e.response.status_code == 404:
                return False  # 404 = pas trouvé
            raise  # Autre erreur = on la relance

# ============================================================================
# UTILISER LE CLIENT DANS ORDERS SERVICE
# ============================================================================

# orders_service/main.py
from fastapi import FastAPI, HTTPException
from pydantic import BaseModel
from typing import List

app = FastAPI(title="Orders Service")

# Créer une instance du client
users_client = UserServiceClient(base_url="http://localhost:8001")

class OrderItem(BaseModel):
    product_id: int
    quantity: int
    price: float

class OrderCreate(BaseModel):
    user_id: int
    items: List[OrderItem]

orders_db = {}

@app.post("/orders")
async def create_order(order: OrderCreate):
    """
    Créer une commande
    
    AVANT de créer la commande, on doit vérifier que l'utilisateur existe!
    """
    # ÉTAPE 1: Vérifier que l'utilisateur existe
    # Appel SYNCHRONE au service Users
    if not users_client.user_exists(order.user_id):
        raise HTTPException(
            status_code=404,
            detail=f"User {order.user_id} not found"
        )
    
    # ÉTAPE 2: Créer la commande
    order_id = len(orders_db) + 1
    total = sum(item.price * item.quantity for item in order.items)
    
    new_order = {
        "id": order_id,
        "user_id": order.user_id,
        "items": [item.dict() for item in order.items],
        "total": total,
        "status": "pending"
    }
    
    orders_db[order_id] = new_order
    
    return new_order

# TEST DU SCÉNARIO COMPLET:
# 1. Démarrer Users Service: uvicorn users_main:app --port 8001
# 2. Démarrer Orders Service: uvicorn orders_main:app --port 8002
# 3. Créer un utilisateur:
#    curl -X POST http://localhost:8001/users \
#      -H "Content-Type: application/json" \
#      -d '{"name": "John", "email": "john@example.com"}'
#    Résultat: {"id": 1, "name": "John", "email": "john@example.com"}
# 
# 4. Créer une commande pour cet utilisateur:
#    curl -X POST http://localhost:8002/orders \
#      -H "Content-Type: application/json" \
#      -d '{
#        "user_id": 1,
#        "items": [
#          {"product_id": 101, "quantity": 2, "price": 29.99}
#        ]
#      }'
#    Résultat: {"id": 1, "user_id": 1, "items": [...], "total": 59.98, "status": "pending"}
#
# 5. Essayer avec utilisateur inexistant:
#    curl -X POST http://localhost:8002/orders \
#      -H "Content-Type: application/json" \
#      -d '{
#        "user_id": 999,
#        "items": [{"product_id": 101, "quantity": 1, "price": 29.99}]
#      }'
#    Résultat: {"detail": "User 999 not found"} (erreur 404)

# ============================================================================
# VERSION ASYNC (Plus performant avec httpx)
# ============================================================================

# requests = synchrone (bloque pendant l'attente)
# httpx = asynchrone (peut faire autre chose pendant l'attente)

pip install httpx  # Client HTTP async

import httpx
import asyncio

class AsyncUserServiceClient:
    """
    Version asynchrone du client
    
    AVANTAGE: Si vous attendez une réponse du service Users,
    FastAPI peut traiter d'autres requêtes en parallèle!
    
    QUAND L'UTILISER:
    - Appels à des services lents
    - Beaucoup de trafic
    - Besoin de haute performance
    """
    
    def __init__(self, base_url="http://localhost:8001"):
        self.base_url = base_url
    
    async def get_user(self, user_id: int):
        """
        Récupérer utilisateur (version async)
        
        DIFFÉRENCE vs sync:
        - Mot-clé 'async' dans la définition
        - Mot-clé 'await' avant les opérations I/O
        - Utilise httpx.AsyncClient au lieu de requests
        """
        # async with = gestion automatique de la connexion
        async with httpx.AsyncClient() as client:
            url = f"{self.base_url}/users/{user_id}"
            
            # await = "attends la réponse, mais laisse d'autres
            #         requêtes se traiter pendant ce temps"
            response = await client.get(url)
            
            response.raise_for_status()
            return response.json()
    
    async def create_user(self, name: str, email: str):
        async with httpx.AsyncClient() as client:
            url = f"{self.base_url}/users"
            data = {"name": name, "email": email}
            
            response = await client.post(url, json=data)
            response.raise_for_status()
            return response.json()
    
    async def user_exists(self, user_id: int) -> bool:
        try:
            await self.get_user(user_id)
            return True
        except httpx.HTTPStatusError as e:
            if e.response.status_code == 404:
                return False
            raise

# Utilisation dans Orders Service (version async)
async_users_client = AsyncUserServiceClient()

@app.post("/orders/async")
async def create_order_async(order: OrderCreate):
    """Version async - plus performante!"""
    
    # await = attendre la réponse
    if not await async_users_client.user_exists(order.user_id):
        raise HTTPException(status_code=404, detail="User not found")
    
    # Reste du code identique...
    order_id = len(orders_db) + 1
    total = sum(item.price * item.quantity for item in order.items)
    
    new_order = {
        "id": order_id,
        "user_id": order.user_id,
        "items": [item.dict() for item in order.items],
        "total": total
    }
    
    orders_db[order_id] = new_order
    return new_order

# ============================================================================
# GESTION DES ERREURS - SUPER IMPORTANT!
# ============================================================================

# PROBLÈME: Si le service Users est DOWN (arrêté, en panne),
#           votre service Orders plante aussi!

# SOLUTION 1: Gérer les erreurs proprement
async def create_order_with_error_handling(order: OrderCreate):
    """Version robuste avec gestion d'erreurs"""
    
    try:
        # Essayer d'appeler le service Users
        if not await async_users_client.user_exists(order.user_id):
            raise HTTPException(status_code=404, detail="User not found")
    
    except httpx.ConnectError:
        # Service Users est DOWN / injoignable
        raise HTTPException(
            status_code=503,  # 503 = Service Unavailable
            detail="Users service is temporarily unavailable"
        )
    
    except httpx.TimeoutException:
        # Service Users est trop lent à répondre
        raise HTTPException(
            status_code=504,  # 504 = Gateway Timeout
            detail="Users service timeout"
        )
    
    except Exception as e:
        # Autre erreur inattendue
        print(f"Unexpected error: {e}")  # Logger l'erreur
        raise HTTPException(
            status_code=500,
            detail="Internal server error"
        )
    
    # Créer la commande si tout va bien
    order_id = len(orders_db) + 1
    # ... reste du code

# SOLUTION 2: Ajouter des timeouts
async def get_user_with_timeout(user_id: int):
    """Appel avec timeout de 5 secondes"""
    async with httpx.AsyncClient(timeout=5.0) as client:
        url = f"http://localhost:8001/users/{user_id}"
        response = await client.get(url)
        return response.json()

# Si le service Users ne répond pas en 5 secondes:
# -> httpx.TimeoutException

# SOLUTION 3: Retry (Réessayer automatiquement)
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(
    stop=stop_after_attempt(3),  # Réessayer max 3 fois
    wait=wait_exponential(multiplier=1, min=2, max=10)  # Attendre 2s, 4s, 8s
)
async def get_user_with_retry(user_id: int):
    """
    Réessaie automatiquement si échec
    
    Utile si:
    - Service temporairement surchargé
    - Problème réseau transitoire
    
    Exemple:
    Tentative 1: Échec -> Attendre 2s
    Tentative 2: Échec -> Attendre 4s
    Tentative 3: Échec -> Lever exception
    """
    async with httpx.AsyncClient(timeout=5.0) as client:
        url = f"http://localhost:8001/users/{user_id}"
        response = await client.get(url)
        response.raise_for_status()
        return response.json()

# ============================================================================
# RÉSUMÉ - COMMUNICATION SYNCHRONE (REST)
# ============================================================================

# AVANTAGES:
# [OK] Simple à comprendre (comme appeler une fonction)
# [OK] Réponse immédiate
# [OK] Facile à débugger
# [OK] Pas besoin d'infrastructure supplémentaire

# INCONVÉNIENTS:
# [X] Service appelé doit être UP (sinon vous êtes bloqué)
# [X] Latence cumulée si chaîne d'appels (A -> B -> C -> D)
# [X] Pas adapté pour opérations longues (> 30 secondes)

# QUAND UTILISER:
# [OK] Besoin de la réponse immédiatement
# [OK] Opération rapide (< 5 secondes)
# [OK] Lire des données (GET)
# [OK] Architecture simple

# QUAND ÉVITER:
# [X] Opération longue (envoi email, traitement vidéo)
# [X] Service appelé souvent DOWN
# [X] Pas besoin de réponse immédiate


# ============================================================================
# MÉTHODE 2: MESSAGE QUEUES (ASYNCHRONE) - POUR OPÉRATIONS LONGUES
# ============================================================================

# RAPPEL ANALOGIE:
# REST = Téléphone (je t'appelle, j'attends ta réponse, je suis bloqué)
# Message Queue = Email (j'envoie, je continue mon travail, tu réponds quand tu peux)

# QU'EST-CE QU'UNE MESSAGE QUEUE ?
# C'est une "boîte aux lettres" où les services déposent et lisent des messages
# 
# Exemple concret:
# Service Orders -> "Hey, envoyez un email de confirmation au client 123"
# Service Notifications -> Lit le message quand il est prêt -> Envoie l'email
# 
# Service Orders n'attend PAS que l'email soit envoyé!
# Il continue à traiter d'autres commandes

# ============================================================================
# RABBITMQ - LE PLUS POPULAIRE
# ============================================================================

# INSTALLATION RABBITMQ
# Option 1: Docker (recommandé pour débuter)
docker run -d --name rabbitmq \
  -p 5672:5672 \
  -p 15672:15672 \
  rabbitmq:3-management

# Port 5672: Communication avec RabbitMQ
# Port 15672: Interface web (http://localhost:15672)
# Login: guest / guest

# Option 2: Installer directement
# Ubuntu: sudo apt-get install rabbitmq-server
# Mac: brew install rabbitmq
# Windows: télécharger depuis rabbitmq.com

pip install pika  # Client Python pour RabbitMQ

# ============================================================================
# CONCEPTS CLÉS RABBITMQ (à comprendre absolument!)
# ============================================================================

# 1. PRODUCER (Producteur)
#    Service qui ENVOIE des messages
#    Exemple: Orders Service envoie "commande créée"

# 2. CONSUMER (Consommateur)
#    Service qui LIT et TRAITE les messages
#    Exemple: Notifications Service lit et envoie emails

# 3. QUEUE (File d'attente)
#    La "boîte aux lettres" où les messages attendent
#    Exemple: queue nommée "email_notifications"

# 4. EXCHANGE (Échange)
#    Le "bureau de poste" qui route les messages vers les bonnes queues
#    Types:
#    - direct: route vers 1 queue spécifique
#    - topic: route selon pattern (user.*, order.created, etc.)
#    - fanout: broadcast vers TOUTES les queues

# 5. ROUTING KEY
#    L'"adresse" sur l'enveloppe du message
#    Exemple: "order.created", "user.updated"

# SCHÉMA SIMPLE:
# 
# Producer -> Exchange -> Queue -> Consumer
#   (envoie)  (route)   (stocke)  (traite)
#
# Producer: Orders Service
# Exchange: "orders" (type topic)
# Routing Key: "order.created"
# Queue: "notifications_queue"
# Consumer: Notifications Service

# ============================================================================
# EXEMPLE COMPLET - SERVICE D'ENVOI D'EMAILS
# ============================================================================

# ÉTAPE 1: CRÉER LE PRODUCER (Orders Service envoie messages)
# orders_service/publisher.py

import pika
import json

class MessagePublisher:
    """
    Classe pour PUBLIER des messages dans RabbitMQ
    
    C'est comme le "facteur" qui dépose les lettres
    """
    
    def __init__(self, host='localhost'):
        """
        Initialiser la connexion à RabbitMQ
        
        host='localhost' en développement
        host='rabbitmq' dans Docker (nom du service)
        """
        # Créer connexion à RabbitMQ
        self.connection = pika.BlockingConnection(
            pika.ConnectionParameters(host=host)
        )
        
        # Créer un "canal" (comme une ligne téléphonique)
        self.channel = self.connection.channel()
    
    def publish_event(self, exchange, routing_key, event):
        """
        Publier un événement dans RabbitMQ
        
        Parameters:
        - exchange: Nom de l'exchange (bureau de poste)
        - routing_key: Clé de routage (adresse)
        - event: Dictionnaire Python avec les données
        
        Exemple d'utilisation:
        publisher.publish_event(
            exchange='orders',
            routing_key='order.created',
            event={'order_id': 123, 'user_id': 456, 'total': 99.99}
        )
        """
        
        # ÉTAPE 1: Déclarer l'exchange (créer si n'existe pas)
        self.channel.exchange_declare(
            exchange=exchange,
            exchange_type='topic',  # Type: topic (routage par pattern)
            durable=True           # durable=True: survit au redémarrage
        )
        
        # ÉTAPE 2: Convertir dictionnaire Python -> JSON string
        message = json.dumps(event)
        
        # ÉTAPE 3: Publier le message
        self.channel.basic_publish(
            exchange=exchange,
            routing_key=routing_key,
            body=message,  # Le message en JSON
            properties=pika.BasicProperties(
                delivery_mode=2,  # 2 = persistent (survit au redémarrage)
                content_type='application/json'
            )
        )
        
        print(f"[OK] Published: {routing_key} -> {message}")
    
    def close(self):
        """Fermer la connexion proprement"""
        self.connection.close()

# UTILISATION DANS ORDERS SERVICE
# orders_service/main.py

from publisher import MessagePublisher

# Créer publisher au démarrage de l'app
publisher = MessagePublisher(host='localhost')

@app.post("/orders")
async def create_order(order: OrderCreate):
    """
    Créer une commande ET envoyer notification
    """
    
    # ÉTAPE 1: Créer la commande dans la base de données
    order_id = len(orders_db) + 1
    new_order = {
        "id": order_id,
        "user_id": order.user_id,
        "items": [item.dict() for item in order.items],
        "status": "created"
    }
    orders_db[order_id] = new_order
    
    # ÉTAPE 2: Publier événement "order.created"
    # On ne BLOQUE PAS pour attendre l'envoi de l'email!
    publisher.publish_event(
        exchange='orders',
        routing_key='order.created',  # Type d'événement
        event={
            'order_id': order_id,
            'user_id': order.user_id,
            'total': sum(item.price * item.quantity for item in order.items),
            'timestamp': datetime.utcnow().isoformat()
        }
    )
    
    # ÉTAPE 3: Retourner immédiatement (ne pas attendre l'email!)
    return new_order

# RÉSULTAT:
# La commande est créée instantanément
# L'email sera envoyé "plus tard" par le Notifications Service
# Si l'envoi d'email échoue, la commande est quand même créée!

# ============================================================================
# ÉTAPE 2: CRÉER LE CONSUMER (Notifications Service lit messages)
# notifications_service/consumer.py

import pika
import json
import time

class MessageConsumer:
    """
    Classe pour CONSOMMER (lire) des messages de RabbitMQ
    
    C'est comme le "destinataire" qui lit ses lettres
    """
    
    def __init__(self, host='localhost'):
        self.connection = pika.BlockingConnection(
            pika.ConnectionParameters(host=host)
        )
        self.channel = self.connection.channel()
    
    def consume(self, exchange, queue, routing_keys, callback):
        """
        Consommer des messages d'une queue
        
        Parameters:
        - exchange: Nom de l'exchange
        - queue: Nom de la queue à créer/lire
        - routing_keys: Liste des routing keys à écouter
        - callback: Fonction à appeler pour chaque message
        
        Exemple:
        consumer.consume(
            exchange='orders',
            queue='notifications',
            routing_keys=['order.created', 'order.cancelled'],
            callback=handle_order_event
        )
        """
        
        # ÉTAPE 1: Déclarer l'exchange (doit exister)
        self.channel.exchange_declare(
            exchange=exchange,
            exchange_type='topic',
            durable=True
        )
        
        # ÉTAPE 2: Créer la queue
        self.channel.queue_declare(
            queue=queue,
            durable=True  # Survit au redémarrage
        )
        
        # ÉTAPE 3: "Binder" (lier) la queue à l'exchange
        # Pour chaque routing_key, créer un lien
        for routing_key in routing_keys:
            self.channel.queue_bind(
                exchange=exchange,
                queue=queue,
                routing_key=routing_key
            )
            print(f"[EMAIL] Listening to: {routing_key}")
        
        # ÉTAPE 4: Définir comment traiter les messages
        def on_message(ch, method, properties, body):
            """
            Appelé pour CHAQUE message reçu
            
            ch: Channel
            method: Metadata (routing_key, etc.)
            properties: Propriétés du message
            body: Le message lui-même (bytes)
            """
            
            # Convertir JSON bytes -> dict Python
            event = json.loads(body)
            
            print(f"[MESSAGE] Received: {method.routing_key} -> {event}")
            
            # Appeler la fonction callback pour traiter l'événement
            callback(event)
            
            # IMPORTANT: Accuser réception (ACK)
            # Si on n'ACK pas, RabbitMQ pense que le traitement a échoué
            # et renverra le message!
            ch.basic_ack(delivery_tag=method.delivery_tag)
        
        # ÉTAPE 5: Configurer QoS (Quality of Service)
        # prefetch_count=1: Traiter 1 message à la fois
        # Empêche un consumer lent d'accumuler trop de messages
        self.channel.basic_qos(prefetch_count=1)
        
        # ÉTAPE 6: Commencer à consommer
        self.channel.basic_consume(
            queue=queue,
            on_message_callback=on_message
            # auto_ack=False par défaut (on ACK manuellement)
        )
        
        print(f"[AUDIO] Consuming from queue: {queue}")
        print("Waiting for messages. Press CTRL+C to exit")
        
        # BOUCLE INFINIE: écoute les messages
        self.channel.start_consuming()

# ÉTAPE 3: CRÉER LA LOGIQUE DE TRAITEMENT
# notifications_service/main.py

def send_email(to_email, subject, body):
    """
    Simuler l'envoi d'un email
    En production: utiliser SendGrid, AWS SES, etc.
    """
    print(f"[EMAIL] Sending email to {to_email}")
    print(f"   Subject: {subject}")
    print(f"   Body: {body}")
    
    # Simuler délai d'envoi
    time.sleep(2)
    
    print(f"[OK] Email sent!")

def handle_order_event(event):
    """
    Fonction appelée quand on reçoit un événement de commande
    
    Cette fonction TRAITE le message
    """
    order_id = event['order_id']
    user_id = event['user_id']
    total = event['total']
    
    # Récupérer email de l'utilisateur (appel à Users Service)
    # Pour simplifier, on simule
    user_email = f"user{user_id}@example.com"
    
    # Envoyer l'email
    send_email(
        to_email=user_email,
        subject=f"Order #{order_id} Confirmation",
        body=f"Thank you! Your order of ${total} has been confirmed."
    )

# LANCER LE CONSUMER
if __name__ == "__main__":
    consumer = MessageConsumer(host='localhost')
    
    # Écouter les événements de commandes
    consumer.consume(
        exchange='orders',
        queue='notifications',
        routing_keys=['order.created', 'order.updated'],
        callback=handle_order_event
    )

# COMMENT TESTER:
# 1. Démarrer RabbitMQ: docker run -d -p 5672:5672 rabbitmq:3
# 2. Lancer Consumer: python notifications_service/main.py
#    (Il attend des messages)
# 3. Lancer Orders Service: uvicorn orders_main:app --port 8002
# 4. Créer une commande:
#    curl -X POST http://localhost:8002/orders \
#      -H "Content-Type: application/json" \
#      -d '{"user_id": 1, "items": [{"product_id": 101, "quantity": 1, "price": 29.99}]}'
# 5. Observer:
#    - Orders Service retourne immédiatement
#    - Consumer affiche "[MESSAGE] Received: order.created -> ..."
#    - Consumer affiche "[EMAIL] Sending email..."
#    - Consumer affiche "[OK] Email sent!"

# ============================================================================
# PATTERNS AVANCÉS RABBITMQ
# ============================================================================

# PATTERN 1: FANOUT (Broadcast à tous)
# Exemple: Événement "user.deleted" -> notifier TOUS les services

# Publisher
publisher.publish_event(
    exchange='users',
    routing_key='user.deleted',
    event={'user_id': 123}
)

# Plusieurs consumers (chacun reçoit le message)
# Service 1: Delete user's orders
# Service 2: Delete user's reviews
# Service 3: Archive user data

# PATTERN 2: TOPIC (Routage par pattern)
# Routing keys avec wildcards:
# - * = 1 mot
# - # = 0 ou plusieurs mots

# Queue 1 écoute: "order.*" (order.created, order.updated, order.cancelled)
# Queue 2 écoute: "order.created" (seulement order.created)
# Queue 3 écoute: "#" (TOUT)

# PATTERN 3: WORK QUEUE (Plusieurs workers)
# 1 queue, plusieurs consumers
# Messages distribués entre les workers (load balancing)

# Exemple: 3 workers écoutent "email_queue"
# Message 1 -> Worker 1
# Message 2 -> Worker 2
# Message 3 -> Worker 3
# Message 4 -> Worker 1 (round-robin)

# ============================================================================
# CELERY - ALTERNATIVE PLUS SIMPLE
# ============================================================================

# Celery = Task queue built on top of RabbitMQ/Redis
# Plus simple que RabbitMQ brut, mais moins flexible

pip install celery redis

# POURQUOI CELERY ?
# - Plus simple que RabbitMQ pur
# - Retry automatique
# - Scheduling (tâches périodiques)
# - Monitoring avec Flower
# - Parfait pour tâches asynchrones simples

# ============================================================================
# CRÉER TÂCHES CELERY
# ============================================================================

# celery_app.py
from celery import Celery

# Créer app Celery
celery_app = Celery(
    'tasks',
    broker='redis://localhost:6379/0',     # Où stocker les messages
    backend='redis://localhost:6379/0'     # Où stocker les résultats
)

# Configuration
celery_app.conf.update(
    task_serializer='json',        # Format des messages
    accept_content=['json'],
    result_serializer='json',
    timezone='UTC',
    enable_utc=True,
)

# DÉFINIR UNE TÂCHE
@celery_app.task(bind=True, max_retries=3)
def send_email_task(self, user_id, email, subject, body):
    """
    Tâche Celery pour envoyer un email
    
    bind=True: Accès à 'self' (l'instance de la tâche)
    max_retries=3: Réessayer max 3 fois si échec
    
    Décorateur @celery_app.task transforme cette fonction
    en tâche asynchrone!
    """
    try:
        # Logique d'envoi email
        print(f"Sending email to {email}")
        
        # Simuler envoi (en prod: SendGrid, AWS SES)
        time.sleep(2)
        
        print(f"Email sent to {email}")
        return {"status": "sent", "email": email}
    
    except Exception as e:
        # Si échec, réessayer avec backoff exponentiel
        # Tentative 1: attendre 60s
        # Tentative 2: attendre 120s (60 * 2^1)
        # Tentative 3: attendre 240s (60 * 2^2)
        raise self.retry(exc=e, countdown=60 * (2 ** self.request.retries))

# Autre exemple: traiter une commande
@celery_app.task
def process_order_task(order_id):
    """Traiter une commande en background"""
    print(f"Processing order {order_id}")
    
    # Logique métier longue
    time.sleep(10)  # Simuler traitement long
    
    return f"Order {order_id} processed"

# ============================================================================
# UTILISER CELERY DANS FASTAPI
# ============================================================================

# orders_service/main.py
from fastapi import FastAPI
from celery_app import send_email_task, process_order_task

app = FastAPI()

@app.post("/orders")
async def create_order(order: OrderCreate):
    """
    Créer commande et lancer tâches asynchrones
    """
    
    # Créer commande dans DB
    order_id = len(orders_db) + 1
    new_order = {
        "id": order_id,
        "user_id": order.user_id,
        "status": "created"
    }
    orders_db[order_id] = new_order
    
    # Lancer tâche asynchrone: envoyer email
    # .delay() = exécuter de manière asynchrone
    send_email_task.delay(
        user_id=order.user_id,
        email="user@example.com",
        subject=f"Order #{order_id} Confirmation",
        body="Thank you for your order!"
    )
    
    # Lancer tâche asynchrone: traiter commande
    process_order_task.delay(order_id)
    
    # Retourner immédiatement (ne pas attendre les tâches!)
    return new_order

# LANCER LE WORKER CELERY
# Terminal 1: Redis
# docker run -d -p 6379:6379 redis

# Terminal 2: Worker Celery
# celery -A celery_app worker --loglevel=info
# 
# Vous verrez:
# - [tasks]
#   . send_email_task
#   . process_order_task
# - Ready to process tasks!

# Terminal 3: FastAPI
# uvicorn main:app --port 8002

# TEST:
# curl -X POST http://localhost:8002/orders \
#   -H "Content-Type: application/json" \
#   -d '{"user_id": 1, "items": [...]}'
#
# Résultat immédiat: {"id": 1, "user_id": 1, "status": "created"}
# Dans logs Celery (2s plus tard): "Email sent to user@example.com"
# Dans logs Celery (10s plus tard): "Order 1 processed"

# ============================================================================
# TÂCHES PÉRIODIQUES (CRON JOBS)
# ============================================================================

# Celery Beat = Planificateur de tâches périodiques
# Comme cron Linux, mais intégré à Celery

from celery.schedules import crontab

# Configuration dans celery_app.py
celery_app.conf.beat_schedule = {
    # Tâche 1: Nettoyer vieilles données tous les jours à 2h du matin
    'cleanup-old-data': {
        'task': 'tasks.cleanup_old_orders',
        'schedule': crontab(hour=2, minute=0),  # Tous les jours à 02:00
    },
    
    # Tâche 2: Envoyer rapport toutes les heures
    'hourly-report': {
        'task': 'tasks.generate_report',
        'schedule': crontab(minute=0),  # Toutes les heures pile
    },
    
    # Tâche 3: Backup tous les lundis à minuit
    'weekly-backup': {
        'task': 'tasks.backup_database',
        'schedule': crontab(hour=0, minute=0, day_of_week=1),  # Lundi
    },
    
    # Tâche 4: Toutes les 30 secondes (pour tests)
    'frequent-task': {
        'task': 'tasks.check_something',
        'schedule': 30.0,  # 30 secondes
    },
}

# Lancer Celery Beat:
# celery -A celery_app beat --loglevel=info

# ============================================================================
# RÉSUMÉ - MESSAGE QUEUES
# ============================================================================

# AVANTAGES:
# [OK] Asynchrone (ne bloque pas le service appelant)
# [OK] Résilience (messages pas perdus si service DOWN)
# [OK] Découplage (services ne se connaissent pas directement)
# [OK] Load balancing (plusieurs workers)
# [OK] Retry automatique possible

# INCONVÉNIENTS:
# [X] Complexité (infrastructure supplémentaire)
# [X] Pas de réponse immédiate
# [X] Plus difficile à débugger
# [X] Nécessite monitoring

# QUAND UTILISER:
# [OK] Opérations longues (envoi email, traitement vidéo)
# [OK] Pas besoin réponse immédiate
# [OK] Tâches en background
# [OK] Event-driven architecture

# RABBITMQ vs CELERY:
# RabbitMQ: Plus flexible, plus complexe, pour architecture avancée
# Celery: Plus simple, parfait pour tâches asynchrones basiques


[OK] DOCKER - CONTAINERISER VOS MICROSERVICES

# ============================================================================
# POURQUOI DOCKER ?
# ============================================================================

# PROBLÈME CLASSIQUE:
# "Ça marche sur ma machine!" [SHRUG]
#
# Votre code fonctionne sur votre laptop:
# - Python 3.11
# - PostgreSQL 15
# - Ubuntu 22.04
#
# Collègue avec:
# - Python 3.9
# - PostgreSQL 13
# - Windows 11
# -> Code ne fonctionne pas! [!]

# SOLUTION: DOCKER
# Docker = créer une "boîte" qui contient:
# - Votre code
# - Python exact version
# - Toutes les dépendances
# - Configuration exacte
#
# Cette "boîte" fonctionne PARTOUT:
# - Laptop Windows
# - Serveur Linux
# - Cloud AWS/Azure/GCP
# -> Comportement identique! [BRAVO]

# ANALOGIE:
# Sans Docker = Envoyer les ingrédients d'une recette
#   (chacun cuisine différemment)
# Avec Docker = Envoyer le plat déjà préparé
#   (tout le monde mange la même chose)

# ============================================================================
# CONCEPTS CLÉS DOCKER
# ============================================================================

# 1. IMAGE
#    Template (modèle) pour créer un container
#    Comme une "photo" de votre application
#    Exemples: python:3.11, postgres:15, nginx:latest

# 2. CONTAINER
#    Instance d'une image EN COURS D'EXÉCUTION
#    Comme un "processus" isolé
#    1 image -> plusieurs containers possibles

# 3. DOCKERFILE
#    Recette pour construire une image
#    Liste les étapes pour setup l'application

# 4. DOCKER COMPOSE
#    Orchestrer plusieurs containers ensemble
#    Exemple: App + Database + Redis en un seul fichier

# ANALOGIE COMPLÈTE:
# Dockerfile = Recette de cuisine
# Image = Photo du plat fini
# Container = Plat servi à table
# Docker Compose = Menu complet (entrée + plat + dessert)

# ============================================================================
# VOTRE PREMIER DOCKERFILE
# ============================================================================

# Fichier: users-service/Dockerfile

# ÉTAPE 1: Choisir l'image de base
# python:3.11-slim = Python 3.11 avec taille optimisée
FROM python:3.11-slim

# POURQUOI 'slim' ?
# python:3.11 = 900 MB (contient tout)
# python:3.11-slim = 120 MB (essentiel seulement)
# python:3.11-alpine = 50 MB (ultra léger, mais compliqué)
# -> slim = bon compromis!

# ÉTAPE 2: Définir le répertoire de travail
# Tous les chemins seront relatifs à /app
WORKDIR /app

# POURQUOI /app ?
# Convention standard Docker
# Vous pourriez utiliser /code, /service, etc.

# ÉTAPE 3: Copier requirements.txt SEULEMENT
# On copie d'abord requirements.txt seul (pas tout le code)
COPY requirements.txt .

# POURQUOI PAS TOUT COPIER D'UN COUP ?
# Docker cache les étapes (layers)
# Si requirements.txt ne change pas -> réutilise cache
# = Build BEAUCOUP plus rapide!
#
# Ordre d'optimisation:
# 1. Copier requirements (change rarement)
# 2. Installer dépendances (long)
# 3. Copier code (change souvent)
# Si code change mais pas requirements -> réutilise cache de l'étape 2!

# ÉTAPE 4: Installer les dépendances
RUN pip install --no-cache-dir -r requirements.txt

# --no-cache-dir: Ne pas garder le cache pip (économise espace)
# -r requirements.txt: Installer depuis le fichier

# ÉTAPE 5: Copier tout le code
COPY . .

# POURQUOI À LA FIN ?
# Le code change souvent
# Si on le copiait au début, chaque modification
# invaliderait TOUT le cache (même pip install!)

# ÉTAPE 6: Exposer le port
EXPOSE 8001

# EXPOSE est INFORMATIF seulement
# Dit "ce service écoute sur le port 8001"
# N'ouvre PAS vraiment le port (fait avec -p lors du run)

# ÉTAPE 7: Healthcheck (optionnel mais recommandé)
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
  CMD python -c "import requests; requests.get('http://localhost:8001/health')"

# Vérifie que le service est UP toutes les 30s
# Si échec 3 fois de suite -> container marqué "unhealthy"
# Kubernetes peut redémarrer automatiquement

# ÉTAPE 8: Commande de démarrage
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]

# CMD = commande par défaut
# Format liste (exec form) recommandé
# --host 0.0.0.0: Écouter sur TOUTES les interfaces (nécessaire dans Docker)

# DOCKERFILE COMPLET (sans commentaires):
"""
FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY . .
EXPOSE 8001
HEALTHCHECK --interval=30s --timeout=3s \
  CMD python -c "import requests; requests.get('http://localhost:8001/health')"
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]
"""

# ============================================================================
# CONSTRUIRE ET LANCER L'IMAGE
# ============================================================================

# ÉTAPE 1: Construire l'image
docker build -t users-service:latest .

# Explication de la commande:
# docker build: Construire une image
# -t users-service:latest: Tag (nom) de l'image
#    users-service = nom
#    latest = version (tag)
# . : Contexte = répertoire actuel

# Vous verrez:
# Step 1/8 : FROM python:3.11-slim
# Step 2/8 : WORKDIR /app
# ...
# Successfully built abc123def456
# Successfully tagged users-service:latest

# ÉTAPE 2: Vérifier l'image créée
docker images

# Résultat:
# REPOSITORY        TAG      IMAGE ID       CREATED         SIZE
# users-service     latest   abc123def456   2 minutes ago   150MB

# ÉTAPE 3: Lancer un container
docker run -d \
  --name users-service-1 \
  -p 8001:8001 \
  users-service:latest

# Explication:
# docker run: Créer et démarrer un container
# -d: Detached mode (en arrière-plan)
# --name users-service-1: Nom du container
# -p 8001:8001: Map port HOST:CONTAINER
#    8001 (gauche) = port sur votre machine
#    8001 (droite) = port dans le container
# users-service:latest: Image à utiliser

# ÉTAPE 4: Vérifier que ça fonctionne
docker ps

# Résultat:
# CONTAINER ID   IMAGE                  STATUS         PORTS
# abc123def456   users-service:latest   Up 2 minutes   0.0.0.0:8001->8001/tcp

# ÉTAPE 5: Tester l'API
curl http://localhost:8001/health
# Résultat: {"status": "healthy"}

# ÉTAPE 6: Voir les logs
docker logs users-service-1

# Ou suivre les logs en temps réel:
docker logs -f users-service-1

# ÉTAPE 7: Arrêter le container
docker stop users-service-1

# ÉTAPE 8: Supprimer le container
docker rm users-service-1

# ÉTAPE 9: Supprimer l'image
docker rmi users-service:latest

# ============================================================================
# DOCKER COMPOSE - ORCHESTRER PLUSIEURS SERVICES
# ============================================================================

# PROBLÈME:
# Architecture microservices typique:
# - Users Service
# - Orders Service
# - PostgreSQL
# - Redis
# - RabbitMQ
#
# Lancer à la main:
# docker run postgres ...
# docker run redis ...
# docker run rabbitmq ...
# docker run users-service ...
# docker run orders-service ...
# -> 5 commandes! [TIRED_FACE]
#
# Et gérer le réseau entre eux? [!]

# SOLUTION: DOCKER COMPOSE
# 1 fichier YAML pour tout définir
# 1 commande pour tout lancer

# ============================================================================
# DOCKER COMPOSE COMPLET - E-COMMERCE
# ============================================================================

# Fichier: docker-compose.yml
version: '3.8'  # Version du format Docker Compose

# DÉFINIR LES SERVICES
services:
  
  # ======== SERVICE 1: PostgreSQL ========
  postgres:
    image: postgres:15  # Image officielle PostgreSQL 15
    container_name: ecommerce-postgres
    environment:
      # Variables d'environnement pour configurer Postgres
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret123
      POSTGRES_DB: ecommerce
    ports:
      - "5432:5432"  # Exposer sur host (pour accès direct)
    volumes:
      # Persister les données (ne pas perdre à l'arrêt)
      - postgres_data:/var/lib/postgresql/data
    networks:
      - microservices  # Réseau partagé entre services
    healthcheck:
      # Vérifier que Postgres est prêt
      test: ["CMD-SHELL", "pg_isready -U admin"]
      interval: 10s
      timeout: 5s
      retries: 5
  
  # ======== SERVICE 2: Redis ========
  redis:
    image: redis:7-alpine  # Redis léger
    container_name: ecommerce-redis
    ports:
      - "6379:6379"
    networks:
      - microservices
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 3s
      retries: 3
  
  # ======== SERVICE 3: RabbitMQ ========
  rabbitmq:
    image: rabbitmq:3-management  # Avec interface web
    container_name: ecommerce-rabbitmq
    environment:
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: admin
    ports:
      - "5672:5672"   # Port AMQP
      - "15672:15672" # Interface web (http://localhost:15672)
    networks:
      - microservices
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "ping"]
      interval: 30s
      timeout: 10s
      retries: 5
  
  # ======== SERVICE 4: Users Service ========
  users-service:
    build:
      context: ./users-service  # Dossier contenant Dockerfile
      dockerfile: Dockerfile
    container_name: users-service
    environment:
      # Variables d'environnement pour l'app
      DATABASE_URL: postgresql://admin:secret123@postgres:5432/ecommerce
      REDIS_URL: redis://redis:6379/0
      # IMPORTANT: Utiliser les NOMS des services (postgres, redis)
      # Docker résout automatiquement les noms -> IPs
    ports:
      - "8001:8001"
    depends_on:
      # Attendre que postgres et redis soient UP
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - microservices
    volumes:
      # Mount code pour développement (hot reload)
      - ./users-service:/app
    command: uvicorn main:app --host 0.0.0.0 --port 8001 --reload
  
  # ======== SERVICE 5: Orders Service ========
  orders-service:
    build:
      context: ./orders-service
      dockerfile: Dockerfile
    container_name: orders-service
    environment:
      DATABASE_URL: postgresql://admin:secret123@postgres:5432/ecommerce
      REDIS_URL: redis://redis:6379/1  # DB 1 pour séparer
      RABBITMQ_URL: amqp://admin:admin@rabbitmq:5672/
      USERS_SERVICE_URL: http://users-service:8001
      # Appel inter-services via nom Docker
    ports:
      - "8002:8002"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
      users-service:
        condition: service_started
    networks:
      - microservices
    volumes:
      - ./orders-service:/app
    command: uvicorn main:app --host 0.0.0.0 --port 8002 --reload
  
  # ======== SERVICE 6: Notifications Service (Consumer) ========
  notifications-service:
    build:
      context: ./notifications-service
      dockerfile: Dockerfile
    container_name: notifications-service
    environment:
      RABBITMQ_URL: amqp://admin:admin@rabbitmq:5672/
    depends_on:
      rabbitmq:
        condition: service_healthy
    networks:
      - microservices
    volumes:
      - ./notifications-service:/app
    command: python consumer.py  # Lance le consumer RabbitMQ

# DÉFINIR LES RÉSEAUX
networks:
  microservices:
    driver: bridge  # Réseau virtuel pour interconnecter les services

# DÉFINIR LES VOLUMES (persistence)
volumes:
  postgres_data:  # Volume nommé pour Postgres

# ============================================================================
# UTILISER DOCKER COMPOSE
# ============================================================================

# COMMANDE 1: Lancer TOUS les services
docker-compose up -d

# Vous verrez:
# Creating network "ecommerce_microservices" ... done
# Creating volume "ecommerce_postgres_data" ... done
# Creating ecommerce-postgres ... done
# Creating ecommerce-redis ... done
# Creating ecommerce-rabbitmq ... done
# Creating users-service ... done
# Creating orders-service ... done
# Creating notifications-service ... done

# -d: Detached mode (arrière-plan)
# Sans -d: Voir les logs en temps réel de TOUS les services

# COMMANDE 2: Voir les services actifs
docker-compose ps

# Résultat:
#         Name                      State           Ports
# -----------------------------------------------------------------
# ecommerce-postgres        Up (healthy)   0.0.0.0:5432->5432/tcp
# ecommerce-redis           Up (healthy)   0.0.0.0:6379->6379/tcp
# ecommerce-rabbitmq        Up (healthy)   0.0.0.0:5672->5672/tcp
# users-service             Up             0.0.0.0:8001->8001/tcp
# orders-service            Up             0.0.0.0:8002->8002/tcp
# notifications-service     Up

# COMMANDE 3: Voir les logs
docker-compose logs -f

# Logs de tous les services mélangés
# Code couleur par service

# Logs d'UN service uniquement:
docker-compose logs -f users-service

# COMMANDE 4: Arrêter tous les services
docker-compose stop

# COMMANDE 5: Arrêter ET supprimer containers
docker-compose down

# COMMANDE 6: Arrêter, supprimer containers ET volumes
docker-compose down -v
# ATTENTION: Efface les données de la base!

# COMMANDE 7: Rebuild les images (après changement Dockerfile)
docker-compose build

# Ou rebuild ET relancer:
docker-compose up -d --build

# COMMANDE 8: Scaler un service (plusieurs instances)
docker-compose up -d --scale orders-service=3

# Lance 3 instances d'orders-service
# Load balancing automatique!

# COMMANDE 9: Exécuter commande dans un service
docker-compose exec users-service bash

# Ouvre un shell dans le container
# Utile pour débugger

# COMMANDE 10: Voir les ressources utilisées
docker-compose stats

# CPU, RAM, Network par service

# ============================================================================
# TESTER L'ARCHITECTURE COMPLÈTE
# ============================================================================

# SCÉNARIO: Créer une commande complète

# ÉTAPE 1: Lancer tout
docker-compose up -d

# ÉTAPE 2: Attendre que tout soit UP (30 secondes)
sleep 30

# ÉTAPE 3: Créer un utilisateur
curl -X POST http://localhost:8001/users \
  -H "Content-Type: application/json" \
  -d '{
    "name": "John Doe",
    "email": "john@example.com"
  }'
# Résultat: {"id": 1, "name": "John Doe", "email": "john@example.com", "active": true}

# ÉTAPE 4: Créer une commande
curl -X POST http://localhost:8002/orders \
  -H "Content-Type: application/json" \
  -d '{
    "user_id": 1,
    "items": [
      {"product_id": 101, "quantity": 2, "price": 29.99},
      {"product_id": 102, "quantity": 1, "price": 49.99}
    ]
  }'
# Résultat: {"id": 1, "user_id": 1, "items": [...], "total": 109.97, "status": "pending"}

# ÉTAPE 5: Vérifier logs notifications-service
docker-compose logs notifications-service

# Vous devriez voir:
# notifications-service | [MESSAGE] Received: order.created -> {"order_id": 1, ...}
# notifications-service | [EMAIL] Sending email to john@example.com
# notifications-service | [OK] Email sent!

# ÉTAPE 6: Vérifier RabbitMQ web UI
# http://localhost:15672
# Login: admin / admin
# Voir les queues, messages, etc.

# ARCHITECTURE EN FONCTIONNEMENT:
# 1. Client -> POST /orders -> Orders Service
# 2. Orders Service -> GET /users/1 -> Users Service (vérif user existe)
# 3. Orders Service -> Publish "order.created" -> RabbitMQ
# 4. Orders Service -> Retourne réponse au client IMMÉDIATEMENT
# 5. Notifications Service (écoute RabbitMQ) -> Reçoit "order.created"
# 6. Notifications Service -> Envoie email
# 
# Total: < 100ms pour le client
# Email envoyé en arrière-plan (2-3s plus tard)

# ============================================================================
# RÉSUMÉ DOCKER
# ============================================================================

# DOCKERFILE:
# - Recette pour construire votre image
# - Optimiser ordre des layers (cache)
# - FROM -> WORKDIR -> COPY requirements -> RUN pip -> COPY code -> CMD

# DOCKER COMPOSE:
# - Orchestrer plusieurs services
# - 1 fichier YAML = toute l'architecture
# - Gère réseau, volumes, dépendances automatiquement
# - docker-compose up: Lancer tout
# - docker-compose down: Arrêter tout

# BONNES PRATIQUES:
# [OK] Utiliser images officielles (python:3.11-slim)
# [OK] Multi-stage builds (optimisation taille)
# [OK] .dockerignore (ne pas copier venv/, __pycache__)
# [OK] Healthchecks obligatoires
# [OK] Variables d'environnement pour config
# [OK] Volumes pour données persistantes
# [OK] Réseaux pour isolation


[OK] KUBERNETES (K8S) - ORCHESTRATION PRODUCTION

# ============================================================================
# POURQUOI KUBERNETES ?
# ============================================================================

# DOCKER COMPOSE: Parfait pour développement
# MAIS en production, vous avez besoin de:
# [X] Redémarrage automatique si crash
# [X] Load balancing intelligent
# [X] Scaling automatique (+ de trafic = + de containers)
# [X] Rolling updates (déploiement sans downtime)
# [X] Self-healing (remplacer containers défaillants)
# [X] Gestion de secrets sécurisée
# [X] Multi-serveurs (cluster)

# KUBERNETES (K8s): Fait TOUT ça automatiquement! [RAPIDE]

# ANALOGIE:
# Docker Compose = Gérer 5 employés dans un petit bureau
# Kubernetes = Gérer 1000 employés dans 50 bureaux différents
#              avec RH automatique, planning intelligent, etc.

# ============================================================================
# CONCEPTS CLÉS KUBERNETES (Important à comprendre!)
# ============================================================================

# 1. CLUSTER
#    Ensemble de serveurs (nodes) qui font tourner vos apps
#    Minimum: 1 master node + 1 worker node
#    Production: 3+ master nodes + 10+ worker nodes

# 2. NODE
#    Un serveur physique ou VM dans le cluster
#    Types:
#    - Master Node: Cerveau (gère le cluster)
#    - Worker Node: Muscles (exécute les apps)

# 3. POD
#    Plus petite unité déployable
#    1 ou plusieurs containers qui tournent ensemble
#    Généralement: 1 pod = 1 container
#    Exemple: 1 pod contient 1 instance de users-service

# 4. DEPLOYMENT
#    Template pour créer et gérer des pods
#    Spécifie: Combien de pods? Quelle image? Quelle config?
#    Gère les updates, rollbacks automatiquement

# 5. SERVICE
#    Point d'accès stable pour accéder aux pods
#    Les pods ont des IPs changeantes -> Service a IP fixe
#    Types:
#    - ClusterIP: Accessible seulement dans le cluster
#    - NodePort: Accessible depuis l'extérieur
#    - LoadBalancer: Load balancer cloud (AWS ELB, etc.)

# 6. NAMESPACE
#    "Dossier" pour organiser ressources
#    Séparer dev/staging/production
#    Par défaut: namespace "default"

# SCHÉMA ARCHITECTURE:
#
# ┌─────────────────── CLUSTER ───────────────────┐
# │                                                │
# │  ┌────── NODE 1 ──────┐  ┌────── NODE 2 ──────┐
# │  │                     │  │                     │
# │  │  ┌─── POD 1 ───┐   │  │  ┌─── POD 2 ───┐   │
# │  │  │ users-service│   │  │  │ users-service│   │
# │  │  └──────────────┘   │  │  └──────────────┘   │
# │  │                     │  │                     │
# │  │  ┌─── POD 3 ───┐   │  │  ┌─── POD 4 ───┐   │
# │  │  │orders-service│   │  │  │orders-service│   │
# │  │  └──────────────┘   │  │  └──────────────┘   │
# │  └─────────────────────┘  └─────────────────────┘
# │                    [BLACK_UP-POINTING_TRIANGLE]                             │
# │                    │                             │
# │              ┌─ SERVICE ─┐                       │
# │              │Load Balance│                      │
# │              └────────────┘                      │
# └────────────────────────────────────────────────┘

# ============================================================================
# INSTALLATION KUBERNETES (LOCAL)
# ============================================================================

# Option 1: MINIKUBE (Recommandé pour apprendre)
# Cluster K8s mono-node sur votre laptop

# macOS:
brew install minikube

# Windows:
choco install minikube

# Linux:
curl -LO https://storage.googleapis.com/minikube/releases/latest/minikube-linux-amd64
sudo install minikube-linux-amd64 /usr/local/bin/minikube

# Démarrer Minikube:
minikube start

# Vous verrez:
# [SMILING_FACE_WITH_OPEN_MOUTH_AND_SMILING_EYES]  minikube v1.32.0 on Darwin 13.0
# *  Using the docker driver based on existing profile
# [BIEN]  Starting control plane node minikube in cluster minikube
# [TRACTOR]  Pulling base image ...
# [HOT]  Creating docker container (CPUs=2, Memory=4000MB) ...
# [DOCKER]  Preparing Kubernetes v1.28.3 on Docker 24.0.7 ...
# [LIEN]  Configuring bridge CNI (Container Networking Interface) ...
# [RECHERCHE]  Verifying Kubernetes components...
# *  Enabled addons: storage-provisioner, default-storageclass
# [SURFER]  Done! kubectl is now configured to use "minikube" cluster

# Option 2: DOCKER DESKTOP
# Inclut Kubernetes intégré
# Settings -> Kubernetes -> Enable Kubernetes

# Option 3: KIND (Kubernetes IN Docker)
# Ultra léger, parfait pour CI/CD
brew install kind
kind create cluster

# ============================================================================
# KUBECTL - L'OUTIL DE LIGNE DE COMMANDE
# ============================================================================

# kubectl = "outil de contrôle Kubernetes"
# Prononcé "koob-cuttle" ou "koob-control"

# Installer kubectl:
brew install kubectl  # macOS
choco install kubernetes-cli  # Windows

# Vérifier installation:
kubectl version --client

# Commandes de base:

# Voir les nodes:
kubectl get nodes
# NAME       STATUS   ROLES           AGE   VERSION
# minikube   Ready    control-plane   5m    v1.28.3

# Voir les pods:
kubectl get pods
# NAME                             READY   STATUS    RESTARTS   AGE
# users-service-abc123-xyz45       1/1     Running   0          2m

# Voir les services:
kubectl get services
# NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)
# kubernetes      ClusterIP   10.96.0.1       <none>        443/TCP
# users-service   ClusterIP   10.96.145.23    <none>        8001/TCP

# Voir les deployments:
kubectl get deployments
# NAME            READY   UP-TO-DATE   AVAILABLE   AGE
# users-service   3/3     3            3           5m

# Voir TOUT:
kubectl get all
# Liste pods, services, deployments, replica sets

# ============================================================================
# VOTRE PREMIER DÉPLOIEMENT KUBERNETES
# ============================================================================

# FICHIER: users-deployment.yaml
# Format YAML (comme docker-compose mais pour Kubernetes)

apiVersion: apps/v1  # Version de l'API Kubernetes
kind: Deployment     # Type de ressource: Deployment
metadata:
  name: users-service  # Nom du deployment
  labels:
    app: users-service  # Label pour identifier
spec:
  # COMBIEN de pods voulez-vous?
  replicas: 3  # 3 instances de users-service
  
  # SÉLECTEUR: Comment trouver les pods gérés par ce deployment?
  selector:
    matchLabels:
      app: users-service  # Gérer les pods avec label app=users-service
  
  # TEMPLATE: Comment créer les pods?
  template:
    metadata:
      labels:
        app: users-service  # Label pour les pods créés
    
    spec:
      # CONTAINERS dans le pod
      containers:
      - name: users-service  # Nom du container
        image: users-service:latest  # Image Docker
        
        # PORTS exposés
        ports:
        - containerPort: 8001  # Port du container
        
        # VARIABLES D'ENVIRONNEMENT
        env:
        - name: DATABASE_URL
          value: "postgresql://admin:secret@postgres:5432/ecommerce"
        - name: REDIS_URL
          value: "redis://redis:6379/0"
        
        # RESSOURCES (CPU, RAM)
        resources:
          # Demandes minimales (garanties)
          requests:
            memory: "256Mi"  # 256 mégaoctets
            cpu: "250m"      # 250 millicores (0.25 CPU)
          # Limites maximales (ne pas dépasser)
          limits:
            memory: "512Mi"
            cpu: "500m"
        
        # HEALTH CHECKS
        # Liveness: Est-ce que l'app est vivante?
        livenessProbe:
          httpGet:
            path: /health
            port: 8001
          initialDelaySeconds: 30  # Attendre 30s au démarrage
          periodSeconds: 10        # Vérifier toutes les 10s
        
        # Readiness: Est-ce que l'app est prête à recevoir du trafic?
        readinessProbe:
          httpGet:
            path: /health
            port: 8001
          initialDelaySeconds: 5
          periodSeconds: 5

# EXPLICATION DES PROBES:
# livenessProbe: Si échec -> Kubernetes REDÉMARRE le pod
# readinessProbe: Si échec -> Kubernetes ARRÊTE d'envoyer du trafic
#
# Exemple:
# - App démarre en 30s -> readinessProbe échoue les 30 premières secondes
#   -> Pas de trafic envoyé pendant ce temps
# - App plante -> livenessProbe échoue
#   -> Kubernetes redémarre automatiquement le pod

# ============================================================================
# CRÉER UN SERVICE KUBERNETES
# ============================================================================

# RAPPEL: Les pods ont des IPs changeantes
# SERVICE = Point d'accès stable (IP fixe + DNS)

# FICHIER: users-service.yaml (même fichier, séparer par ---)

---
apiVersion: v1
kind: Service  # Type: Service
metadata:
  name: users-service
spec:
  # SÉLECTEUR: Quels pods ce service expose?
  selector:
    app: users-service  # Tous les pods avec label app=users-service
  
  # PORTS
  ports:
  - protocol: TCP
    port: 8001         # Port du service (dans le cluster)
    targetPort: 8001   # Port du container
  
  # TYPE de service
  type: ClusterIP  # Accessible uniquement dans le cluster

# TYPES DE SERVICE:
# 
# 1. ClusterIP (par défaut)
#    IP interne uniquement
#    Autres services peuvent l'appeler: http://users-service:8001
#    Pas accessible depuis l'extérieur
#
# 2. NodePort
#    Expose sur un port de CHAQUE node (30000-32767)
#    Accessible: http://<node-ip>:30001
#    Utile pour tests
#
# 3. LoadBalancer
#    Crée un load balancer externe (AWS ELB, GCP LB, etc.)
#    Accessible: http://<load-balancer-ip>
#    Coûte de l'argent (service cloud)

# Exemple NodePort (pour accès externe):
---
apiVersion: v1
kind: Service
metadata:
  name: users-service-external
spec:
  selector:
    app: users-service
  ports:
  - protocol: TCP
    port: 8001
    targetPort: 8001
    nodePort: 30001  # Port sur le node (30000-32767)
  type: NodePort

# ============================================================================
# DÉPLOYER SUR KUBERNETES
# ============================================================================

# ÉTAPE 1: Créer les ressources
kubectl apply -f users-deployment.yaml

# Résultat:
# deployment.apps/users-service created
# service/users-service created

# ÉTAPE 2: Vérifier les pods créés
kubectl get pods

# NAME                             READY   STATUS    RESTARTS   AGE
# users-service-abc123-def45       1/1     Running   0          30s
# users-service-abc123-ghi78       1/1     Running   0          30s
# users-service-abc123-jkl90       1/1     Running   0          30s
# 
# 3 pods créés (replicas: 3)!

# ÉTAPE 3: Voir les détails d'un pod
kubectl describe pod users-service-abc123-def45

# Montre:
# - État du pod
# - Events (création, pulling image, démarrage)
# - Resources utilisées
# - Health checks

# ÉTAPE 4: Voir les logs d'un pod
kubectl logs users-service-abc123-def45

# Suivre les logs en temps réel:
kubectl logs -f users-service-abc123-def45

# Logs de TOUS les pods du deployment:
kubectl logs -l app=users-service

# ÉTAPE 5: Tester le service
# Depuis l'intérieur du cluster:
kubectl run test-pod --image=curlimages/curl -it --rm -- sh
# Vous êtes maintenant dans un pod temporaire
curl http://users-service:8001/health
# Résultat: {"status": "healthy"}
exit  # Sortir et supprimer le pod

# Depuis l'extérieur (si NodePort):
minikube service users-service-external
# Ouvre automatiquement dans le navigateur

# Ou manuellement:
minikube ip  # Obtenir IP du cluster (ex: 192.168.49.2)
curl http://192.168.49.2:30001/health

# ============================================================================
# SCALING (Augmenter/Réduire le nombre de pods)
# ============================================================================

# Méthode 1: Modifier replicas dans le YAML
# users-deployment.yaml
spec:
  replicas: 5  # Passer de 3 à 5

kubectl apply -f users-deployment.yaml

# Méthode 2: Commande directe (temporaire)
kubectl scale deployment users-service --replicas=5

# Vérifier:
kubectl get pods
# 5 pods maintenant!

# Réduire:
kubectl scale deployment users-service --replicas=2

# ============================================================================
# AUTOSCALING (Scaling automatique)
# ============================================================================

# HPA = Horizontal Pod Autoscaler
# Augmente/réduit automatiquement selon CPU/RAM

# FICHIER: users-hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: users-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: users-service
  
  # MIN/MAX pods
  minReplicas: 3   # Minimum: toujours 3 pods
  maxReplicas: 10  # Maximum: jamais plus de 10
  
  # MÉTRIQUES pour décider du scaling
  metrics:
  # Métrique 1: CPU
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70  # Si CPU > 70% -> scale up
  
  # Métrique 2: Memory
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80  # Si RAM > 80% -> scale up

# Déployer:
kubectl apply -f users-hpa.yaml

# Vérifier:
kubectl get hpa

# NAME                 REFERENCE                  TARGETS         MINPODS   MAXPODS   REPLICAS
# users-service-hpa    Deployment/users-service   45%/70%, 60%/80%   3         10        3

# EXPLICATION:
# CPU actuel: 45% (cible: 70%) -> OK, pas besoin de scale
# Memory actuelle: 60% (cible: 80%) -> OK
# Si CPU monte à 75% -> Kubernetes ajoute automatiquement des pods!
# Si CPU descend à 30% -> Kubernetes réduit automatiquement

# ============================================================================
# ROLLING UPDATES (Mise à jour sans downtime)
# ============================================================================

# SCÉNARIO: Nouvelle version de users-service (v2.0)

# ÉTAPE 1: Build nouvelle image
docker build -t users-service:v2.0 .

# ÉTAPE 2: Push vers registry (Docker Hub, GCR, ECR)
docker push mycompany/users-service:v2.0

# ÉTAPE 3: Modifier deployment
# users-deployment.yaml
spec:
  template:
    spec:
      containers:
      - name: users-service
        image: mycompany/users-service:v2.0  # <- Changer ici

# ÉTAPE 4: Appliquer
kubectl apply -f users-deployment.yaml

# OU commande directe:
kubectl set image deployment/users-service \
  users-service=mycompany/users-service:v2.0

# QUE SE PASSE-T-IL?
# Kubernetes fait un "rolling update":
# 1. Créer 1 nouveau pod (v2.0)
# 2. Attendre qu'il soit Ready
# 3. Supprimer 1 ancien pod (v1.0)
# 4. Répéter jusqu'à remplacer tous les pods
# 
# Résultat: ZÉRO DOWNTIME! [BRAVO]

# Voir le rollout en temps réel:
kubectl rollout status deployment/users-service

# Résultat:
# Waiting for deployment "users-service" rollout to finish: 1 out of 3 new replicas have been updated...
# Waiting for deployment "users-service" rollout to finish: 2 out of 3 new replicas have been updated...
# Waiting for deployment "users-service" rollout to finish: 2 old replicas are pending termination...
# deployment "users-service" successfully rolled out

# ROLLBACK (annuler la mise à jour)
# Si v2.0 a un bug:
kubectl rollout undo deployment/users-service

# Revenir à une version spécifique:
kubectl rollout undo deployment/users-service --to-revision=1

# Voir historique:
kubectl rollout history deployment/users-service

# REVISION  CHANGE-CAUSE
# 1         Initial deployment
# 2         Update to v2.0
# 3         Update to v2.1

# ============================================================================
# CONFIGMAP (Configuration externe)
# ============================================================================

# PROBLÈME: Variables d'env hardcodées dans deployment
# SOLUTION: ConfigMap (stocker config séparément)

# FICHIER: users-configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: users-config
data:
  # Clé-valeur simple
  LOG_LEVEL: "INFO"
  CACHE_TTL: "300"
  MAX_CONNECTIONS: "100"
  
  # Ou fichier entier
  app.properties: |
    database.pool.size=20
    database.timeout=30
    api.rate.limit=1000

# Déployer:
kubectl apply -f users-configmap.yaml

# Utiliser dans Deployment:
spec:
  template:
    spec:
      containers:
      - name: users-service
        image: users-service:latest
        envFrom:
        - configMapRef:
            name: users-config  # Charger TOUTES les variables

# Ou variable par variable:
        env:
        - name: LOG_LEVEL
          valueFrom:
            configMapKeyRef:
              name: users-config
              key: LOG_LEVEL

# ============================================================================
# SECRETS (Données sensibles)
# ============================================================================

# ConfigMap = données publiques (config)
# Secret = données sensibles (passwords, API keys)

# FICHIER: users-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: users-secret
type: Opaque
stringData:  # stringData = texte plain (Kubernetes encode en base64 auto)
  DATABASE_PASSWORD: "super-secret-password"
  JWT_SECRET_KEY: "my-jwt-secret-key-change-in-prod"
  AWS_ACCESS_KEY: "AKIAIOSFODNN7EXAMPLE"

# OU avec data (déjà encodé en base64):
data:
  DATABASE_PASSWORD: c3VwZXItc2VjcmV0LXBhc3N3b3Jk  # base64

# Déployer:
kubectl apply -f users-secret.yaml

# Utiliser dans Deployment:
spec:
  template:
    spec:
      containers:
      - name: users-service
        env:
        - name: DATABASE_PASSWORD
          valueFrom:
            secretKeyRef:
              name: users-secret
              key: DATABASE_PASSWORD

# IMPORTANT: Secrets ne sont PAS vraiment sécurisés par défaut!
# En production, utiliser:
# - Sealed Secrets
# - External Secrets Operator
# - HashiCorp Vault
# - Cloud provider secrets (AWS Secrets Manager, GCP Secret Manager)

# ============================================================================
# INGRESS (Exposer vos services au monde)
# ============================================================================

# PROBLÈME:
# Vous avez 5 services (users, orders, products, etc.)
# Vous voulez les exposer sur:
# - api.example.com/users
# - api.example.com/orders
# - api.example.com/products
#
# LoadBalancer par service = 5 Load Balancers = $$

# SOLUTION: INGRESS
# 1 seul Load Balancer
# Routage intelligent basé sur l'URL

# Installer Ingress Controller (NGINX):
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/controller-v1.8.1/deploy/static/provider/cloud/deploy.yaml

# Ou avec Minikube:
minikube addons enable ingress

# FICHIER: ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: microservices-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  # Règle 1: api.example.com/users -> users-service
  - host: api.example.com
    http:
      paths:
      - path: /users
        pathType: Prefix
        backend:
          service:
            name: users-service
            port:
              number: 8001
      
      # Règle 2: api.example.com/orders -> orders-service
      - path: /orders
        pathType: Prefix
        backend:
          service:
            name: orders-service
            port:
              number: 8002
      
      # Règle 3: api.example.com/products -> products-service
      - path: /products
        pathType: Prefix
        backend:
          service:
            name: products-service
            port:
              number: 8003

# Déployer:
kubectl apply -f ingress.yaml

# Tester (avec Minikube):
minikube ip  # Ex: 192.168.49.2
# Ajouter à /etc/hosts:
# 192.168.49.2 api.example.com

curl http://api.example.com/users/health
# Routé vers users-service!

curl http://api.example.com/orders
# Routé vers orders-service!

# ============================================================================
# NAMESPACES (Organiser vos ressources)
# ============================================================================

# NAMESPACE = "Dossier" pour séparer ressources

# Créer namespaces:
kubectl create namespace development
kubectl create namespace staging
kubectl create namespace production

# Déployer dans un namespace spécifique:
kubectl apply -f users-deployment.yaml -n development

# Voir pods d'un namespace:
kubectl get pods -n development

# Voir pods de TOUS les namespaces:
kubectl get pods --all-namespaces

# Définir namespace par défaut:
kubectl config set-context --current --namespace=development

# ORGANISATION TYPIQUE:
# - development: Tests locaux
# - staging: Environnement de pré-production
# - production: Production réelle

# ============================================================================
# COMMANDES KUBERNETES ESSENTIELLES (CHEATSHEET)
# ============================================================================

# CRÉER/APPLIQUER
kubectl apply -f deployment.yaml       # Créer/mettre à jour ressource
kubectl create -f deployment.yaml      # Créer seulement (erreur si existe)

# VOIR (GET)
kubectl get pods                       # Lister pods
kubectl get deployments                # Lister deployments
kubectl get services                   # Lister services
kubectl get all                        # Tout lister
kubectl get pods -o wide               # Plus de détails
kubectl get pods --watch               # Surveiller en temps réel

# DÉTAILS (DESCRIBE)
kubectl describe pod <pod-name>        # Détails d'un pod
kubectl describe deployment <name>     # Détails d'un deployment

# LOGS
kubectl logs <pod-name>                # Logs d'un pod
kubectl logs -f <pod-name>             # Suivre logs
kubectl logs <pod-name> --previous     # Logs du pod précédent (si crash)

# EXÉCUTER COMMANDES
kubectl exec -it <pod-name> -- bash    # Shell interactif dans pod
kubectl exec <pod-name> -- ls /app     # Commande unique

# SCALING
kubectl scale deployment <name> --replicas=5   # Scaler manuellement

# ROLLING UPDATES
kubectl set image deployment/<name> container=image:tag
kubectl rollout status deployment/<name>
kubectl rollout undo deployment/<name>
kubectl rollout history deployment/<name>

# SUPPRIMER
kubectl delete pod <pod-name>          # Supprimer pod
kubectl delete deployment <name>       # Supprimer deployment
kubectl delete -f deployment.yaml      # Supprimer selon fichier
kubectl delete all --all               # Supprimer TOUT (DANGER!)

# PORT FORWARD (accès local)
kubectl port-forward pod/<pod-name> 8080:8001
# Accès: http://localhost:8080

kubectl port-forward service/users-service 8080:8001
# Accès: http://localhost:8080

# COPIER FICHIERS
kubectl cp <pod-name>:/path/file ./local-file  # Pod -> Local
kubectl cp ./local-file <pod-name>:/path/file  # Local -> Pod

# DEBUG
kubectl top pods                       # CPU/RAM par pod
kubectl top nodes                      # CPU/RAM par node

# ============================================================================
# RÉSUMÉ KUBERNETES
# ============================================================================

# CONCEPTS CLÉS:
# - Pod: Plus petite unité (1+ containers)
# - Deployment: Gérer pods (replicas, updates)
# - Service: Point d'accès stable (IP fixe)
# - ConfigMap: Configuration (non-sensible)
# - Secret: Données sensibles
# - Ingress: Exposer HTTP(S) au monde
# - Namespace: Organiser ressources

# WORKFLOW TYPIQUE:
# 1. Développer en local (Docker Compose)
# 2. Tester sur Minikube (Kubernetes local)
# 3. Déployer sur cluster staging
# 4. Déployer sur cluster production

# BONNES PRATIQUES:
# [OK] Toujours définir resources (requests/limits)
# [OK] Toujours définir health checks (liveness/readiness)
# [OK] Utiliser namespaces pour séparer envs
# [OK] Utiliser ConfigMaps/Secrets (pas hardcoder)
# [OK] Versionner images Docker (pas :latest en prod)
# [OK] Utiliser Ingress (pas LoadBalancer par service)
# [OK] Monitorer (Prometheus + Grafana)


[OK] AUTHENTIFICATION JWT - SÉCURISER VOS MICROSERVICES

# ============================================================================
# POURQUOI JWT (JSON Web Token) ?
# ============================================================================

# PROBLÈME EN MICROSERVICES:
# Client fait 10 requêtes -> traverse 5 services différents
# Comment savoir QUI est le client à chaque étape?
#
# SOLUTION TRADITIONNELLE (Sessions):
# [X] Sessions stockées en mémoire serveur
# [X] Ne scale pas (besoin session partagée entre services)
# [X] Requête DB à chaque fois
#
# SOLUTION JWT:
# [OK] Token contient TOUTES les infos nécessaires
# [OK] Pas besoin DB pour vérifier
# [OK] Stateless (parfait pour microservices)
# [OK] Auto-suffisant

# ANALOGIE:
# Session = Badge d'accès (garde doit vérifier dans registre)
# JWT = Carte d'identité (toutes les infos dessus, auto-vérifiable)

# ============================================================================
# STRUCTURE D'UN JWT
# ============================================================================

# JWT = 3 parties séparées par des points:
# HEADER.PAYLOAD.SIGNATURE

# Exemple de JWT:
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c

# PARTIE 1: HEADER (encodé en base64)
# {
#   "alg": "HS256",    # Algorithme de signature
#   "typ": "JWT"       # Type: JWT
# }

# PARTIE 2: PAYLOAD (encodé en base64)
# {
#   "sub": "1234567890",         # Subject: ID utilisateur
#   "name": "John Doe",          # Données custom
#   "email": "john@example.com",
#   "role": "admin",
#   "iat": 1516239022,           # Issued At: timestamp création
#   "exp": 1516242622            # Expiration: timestamp expiration
# }

# PARTIE 3: SIGNATURE
# HMACSHA256(
#   base64UrlEncode(header) + "." + base64UrlEncode(payload),
#   secret_key
# )
# -> Garantit que le token n'a pas été modifié!

# IMPORTANT: JWT n'est PAS CHIFFRÉ (juste encodé base64)
# Tout le monde peut lire le contenu!
# Ne JAMAIS mettre de données sensibles (passwords, numéros carte)

# ============================================================================
# CRÉER UN SYSTÈME D'AUTHENTIFICATION JWT
# ============================================================================

pip install pyjwt python-jose[cryptography] passlib[bcrypt]

# FICHIER: auth/auth.py

from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import HTTPException, Security, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel

# ============================================================================
# CONFIGURATION
# ============================================================================

# SECRET KEY: Clé secrète pour signer les tokens
# EN PRODUCTION: Générer avec secrets.token_urlsafe(32)
# NE JAMAIS committer dans Git!
SECRET_KEY = "your-super-secret-key-change-in-production-min-32-chars"

# ALGORITHM: Algorithme de signature
ALGORITHM = "HS256"  # HMAC SHA-256

# DURÉE DE VIE DES TOKENS
ACCESS_TOKEN_EXPIRE_MINUTES = 30   # Token d'accès: 30 min
REFRESH_TOKEN_EXPIRE_DAYS = 7      # Token refresh: 7 jours

# ============================================================================
# HASHING DES PASSWORDS (BCRYPT)
# ============================================================================

# JAMAIS stocker passwords en clair!
# Bcrypt = algorithme de hashing sécurisé

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")

def hash_password(password: str) -> str:
    """
    Hasher un password
    
    Exemple:
    password: "mypassword123"
    hash: "$2b$12$KIXxLV39YR8z5Q.../..."  <- Stocké en DB
    
    Chaque fois différent (salt automatique)!
    """
    return pwd_context.hash(password)

def verify_password(plain_password: str, hashed_password: str) -> bool:
    """
    Vérifier un password
    
    Comparer password saisi vs hash stocké
    """
    return pwd_context.verify(plain_password, hashed_password)

# Exemple d'utilisation:
# Inscription:
# hashed = hash_password("mypassword123")
# db.save(user_id, hashed)  # Stocker le hash
#
# Login:
# stored_hash = db.get_password(user_id)
# if verify_password("mypassword123", stored_hash):
#     print("Login OK!")

# ============================================================================
# CRÉER ET VÉRIFIER TOKENS JWT
# ============================================================================

def create_access_token(data: dict, expires_delta: timedelta = None):
    """
    Créer un JWT access token
    
    Parameters:
    - data: Dict avec infos utilisateur {"sub": "user123", "role": "admin"}
    - expires_delta: Durée de vie (optionnel)
    
    Returns:
    - String JWT token
    """
    # Copier données pour ne pas modifier l'original
    to_encode = data.copy()
    
    # Calculer expiration
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    
    # Ajouter expiration au payload
    to_encode.update({"exp": expire})
    
    # Créer le JWT
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    
    return encoded_jwt

# Exemple d'utilisation:
# token = create_access_token(
#     data={"sub": "user123", "role": "admin"},
#     expires_delta=timedelta(minutes=30)
# )
# -> "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...."

def decode_token(token: str) -> dict:
    """
    Décoder et vérifier un JWT
    
    Returns:
    - Dict avec payload si valide
    
    Raises:
    - JWTError si invalide ou expiré
    """
    try:
        # Décoder le JWT
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        
        # Vérifier qu'il y a un "sub" (user ID)
        user_id: str = payload.get("sub")
        if user_id is None:
            raise HTTPException(
                status_code=401,
                detail="Invalid token: missing subject"
            )
        
        return payload
    
    except JWTError as e:
        # Token invalide ou expiré
        raise HTTPException(
            status_code=401,
            detail=f"Invalid token: {str(e)}",
            headers={"WWW-Authenticate": "Bearer"}
        )

# ============================================================================
# DÉPENDANCE FASTAPI POUR VÉRIFIER TOKENS
# ============================================================================

# HTTPBearer = Scheme pour tokens dans header Authorization
security = HTTPBearer()

def get_current_user(
    credentials: HTTPAuthorizationCredentials = Security(security)
) -> dict:
    """
    Dépendance FastAPI pour extraire et vérifier le token
    
    Utilisation:
    @app.get("/protected")
    async def protected_route(user = Depends(get_current_user)):
        return {"user": user}
    
    Client doit envoyer:
    Authorization: Bearer <token>
    """
    # Extraire le token du header Authorization
    token = credentials.credentials
    
    # Décoder et vérifier
    payload = decode_token(token)
    
    return payload

# ============================================================================
# MODÈLES PYDANTIC
# ============================================================================

class UserLogin(BaseModel):
    """Données de login"""
    email: str
    password: str

class UserRegister(BaseModel):
    """Données d'inscription"""
    email: str
    name: str
    password: str

class Token(BaseModel):
    """Réponse avec tokens"""
    access_token: str
    refresh_token: str
    token_type: str = "bearer"

class User(BaseModel):
    """Modèle utilisateur (sans password!)"""
    id: int
    email: str
    name: str
    role: str = "user"

# ============================================================================
# ENDPOINTS D'AUTHENTIFICATION
# ============================================================================

from fastapi import FastAPI

app = FastAPI(title="Auth Service")

# Base de données simulée
users_db = {}  # {user_id: {"email": "...", "password_hash": "...", ...}}
user_id_counter = 0

@app.post("/register", response_model=User, status_code=201)
async def register(user_data: UserRegister):
    """
    Inscription d'un nouvel utilisateur
    
    POST /register
    Body: {"email": "john@example.com", "name": "John", "password": "secret123"}
    
    Returns: User créé (sans password!)
    """
    global user_id_counter
    
    # Vérifier si email existe déjà
    for user in users_db.values():
        if user["email"] == user_data.email:
            raise HTTPException(
                status_code=400,
                detail="Email already registered"
            )
    
    # Hasher le password
    password_hash = hash_password(user_data.password)
    
    # Créer utilisateur
    user_id_counter += 1
    new_user = {
        "id": user_id_counter,
        "email": user_data.email,
        "name": user_data.name,
        "password_hash": password_hash,
        "role": "user"
    }
    
    users_db[user_id_counter] = new_user
    
    # Retourner user SANS password
    return User(
        id=new_user["id"],
        email=new_user["email"],
        name=new_user["name"],
        role=new_user["role"]
    )

@app.post("/login", response_model=Token)
async def login(credentials: UserLogin):
    """
    Login et génération de tokens
    
    POST /login
    Body: {"email": "john@example.com", "password": "secret123"}
    
    Returns: Access token + Refresh token
    """
    
    # ÉTAPE 1: Trouver utilisateur par email
    user = None
    for u in users_db.values():
        if u["email"] == credentials.email:
            user = u
            break
    
    if not user:
        raise HTTPException(
            status_code=401,
            detail="Incorrect email or password"
        )
    
    # ÉTAPE 2: Vérifier password
    if not verify_password(credentials.password, user["password_hash"]):
        raise HTTPException(
            status_code=401,
            detail="Incorrect email or password"
        )
    
    # ÉTAPE 3: Créer access token
    access_token = create_access_token(
        data={
            "sub": str(user["id"]),      # Subject: user ID
            "email": user["email"],
            "role": user["role"]
        },
        expires_delta=timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    )
    
    # ÉTAPE 4: Créer refresh token (durée plus longue)
    refresh_token = create_access_token(
        data={
            "sub": str(user["id"]),
            "type": "refresh"  # Marquer comme refresh token
        },
        expires_delta=timedelta(days=REFRESH_TOKEN_EXPIRE_DAYS)
    )
    
    # ÉTAPE 5: Retourner les deux tokens
    return Token(
        access_token=access_token,
        refresh_token=refresh_token,
        token_type="bearer"
    )

@app.get("/me", response_model=User)
async def get_current_user_info(current_user = Depends(get_current_user)):
    """
    Récupérer info utilisateur connecté
    
    GET /me
    Header: Authorization: Bearer <access_token>
    
    Returns: Info utilisateur
    """
    # current_user = payload du JWT
    user_id = int(current_user["sub"])
    
    if user_id not in users_db:
        raise HTTPException(status_code=404, detail="User not found")
    
    user = users_db[user_id]
    
    return User(
        id=user["id"],
        email=user["email"],
        name=user["name"],
        role=user["role"]
    )

@app.post("/refresh", response_model=Token)
async def refresh_access_token(current_user = Depends(get_current_user)):
    """
    Rafraîchir l'access token avec refresh token
    
    POST /refresh
    Header: Authorization: Bearer <refresh_token>
    
    Returns: Nouveau access token
    """
    # Vérifier que c'est bien un refresh token
    if current_user.get("type") != "refresh":
        raise HTTPException(
            status_code=400,
            detail="Invalid token type (need refresh token)"
        )
    
    user_id = int(current_user["sub"])
    user = users_db.get(user_id)
    
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    
    # Créer nouveau access token
    new_access_token = create_access_token(
        data={
            "sub": str(user["id"]),
            "email": user["email"],
            "role": user["role"]
        }
    )
    
    return Token(
        access_token=new_access_token,
        refresh_token=current_user.get("refresh_token", ""),
        token_type="bearer"
    )

# ============================================================================
# ROUTES PROTÉGÉES (nécessitent authentification)
# ============================================================================

@app.get("/admin/users")
async def list_all_users(current_user = Depends(get_current_user)):
    """
    Route protégée - seulement pour admins
    
    GET /admin/users
    Header: Authorization: Bearer <access_token>
    """
    
    # Vérifier que l'utilisateur est admin
    if current_user.get("role") != "admin":
        raise HTTPException(
            status_code=403,
            detail="Admin access required"
        )
    
    # Retourner liste users (sans passwords)
    return [
        {
            "id": u["id"],
            "email": u["email"],
            "name": u["name"],
            "role": u["role"]
        }
        for u in users_db.values()
    ]

# ============================================================================
# TESTER LE FLOW COMPLET
# ============================================================================

# ÉTAPE 1: Inscription
# curl -X POST http://localhost:8000/register \
#   -H "Content-Type: application/json" \
#   -d '{"email": "john@example.com", "name": "John Doe", "password": "secret123"}'
#
# Résultat:
# {"id": 1, "email": "john@example.com", "name": "John Doe", "role": "user"}

# ÉTAPE 2: Login
# curl -X POST http://localhost:8000/login \
#   -H "Content-Type: application/json" \
#   -d '{"email": "john@example.com", "password": "secret123"}'
#
# Résultat:
# {
#   "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
#   "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
#   "token_type": "bearer"
# }

# ÉTAPE 3: Utiliser access token pour route protégée
# curl http://localhost:8000/me \
#   -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
#
# Résultat:
# {"id": 1, "email": "john@example.com", "name": "John Doe", "role": "user"}

# ÉTAPE 4: Essayer sans token (erreur)
# curl http://localhost:8000/me
#
# Résultat (erreur 401):
# {"detail": "Not authenticated"}

# ÉTAPE 5: Essayer avec token invalide (erreur)
# curl http://localhost:8000/me \
#   -H "Authorization: Bearer invalid-token"
#
# Résultat (erreur 401):
# {"detail": "Invalid token"}

# ============================================================================
# UTILISER JWT DANS D'AUTRES MICROSERVICES
# ============================================================================

# SCÉNARIO: Orders Service doit vérifier que l'utilisateur est authentifié

# orders_service/auth.py (copier le code de vérification)
from auth import get_current_user  # Réutiliser la dépendance

@app.post("/orders")
async def create_order(
    order: OrderCreate,
    current_user = Depends(get_current_user)  # <- Nécessite JWT!
):
    """
    Créer commande (authentification requise)
    
    POST /orders
    Header: Authorization: Bearer <access_token>
    Body: {"items": [...]}
    """
    
    # current_user contient le payload du JWT
    user_id = int(current_user["sub"])
    user_email = current_user["email"]
    
    # Créer commande pour cet utilisateur
    new_order = {
        "id": len(orders_db) + 1,
        "user_id": user_id,
        "user_email": user_email,
        "items": order.items,
        "status": "created"
    }
    
    orders_db[new_order["id"]] = new_order
    
    return new_order

# CLIENT APPELLE:
# 1. Login sur Auth Service -> Obtenir token
# 2. Utiliser token pour Orders Service
#
# curl -X POST http://localhost:8002/orders \
#   -H "Authorization: Bearer <token>" \
#   -H "Content-Type: application/json" \
#   -d '{"items": [...]}'

# ============================================================================
# API GATEWAY: CENTRALISER L'AUTHENTIFICATION
# ============================================================================

# MEILLEURE PRATIQUE: Vérifier JWT au niveau API Gateway
# Éviter de dupliquer le code dans chaque service

# gateway/main.py
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import Response
import httpx
from auth import decode_token  # Fonction de vérification JWT

app = FastAPI(title="API Gateway")

# Configuration des services backend
SERVICES = {
    "users": "http://localhost:8001",
    "orders": "http://localhost:8002",
    "products": "http://localhost:8003",
}

@app.middleware("http")
async def verify_jwt_middleware(request: Request, call_next):
    """
    Middleware: Vérifier JWT pour toutes les requêtes (sauf login/register)
    """
    
    # Exclure routes publiques
    public_paths = ["/login", "/register", "/health", "/docs"]
    if any(request.url.path.startswith(path) for path in public_paths):
        # Route publique -> pas de vérification
        return await call_next(request)
    
    # Extraire token du header Authorization
    auth_header = request.headers.get("Authorization")
    if not auth_header or not auth_header.startswith("Bearer "):
        raise HTTPException(
            status_code=401,
            detail="Missing or invalid Authorization header"
        )
    
    token = auth_header.split(" ")[1]
    
    # Vérifier le token
    try:
        payload = decode_token(token)
        # Ajouter payload au state de la requête (accessible dans routes)
        request.state.user = payload
    except HTTPException:
        raise
    
    # Continuer vers la route
    response = await call_next(request)
    return response

@app.api_route("/orders/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def orders_proxy(path: str, request: Request):
    """
    Proxy vers Orders Service
    JWT déjà vérifié par middleware!
    """
    
    # User info disponible via request.state.user
    user_id = request.state.user["sub"]
    
    # Forward la requête au service Orders
    async with httpx.AsyncClient() as client:
        url = f"{SERVICES['orders']}/orders/{path}"
        
        # Ajouter user_id dans header pour le service backend
        headers = dict(request.headers)
        headers["X-User-ID"] = user_id
        
        response = await client.request(
            method=request.method,
            url=url,
            headers=headers,
            content=await request.body()
        )
        
        return Response(
            content=response.content,
            status_code=response.status_code,
            headers=dict(response.headers)
        )

# AVANTAGE:
# - JWT vérifié UNE SEULE FOIS (au gateway)
# - Services backend reçoivent X-User-ID (pas besoin vérifier JWT)
# - Centralisation de la logique auth

# ============================================================================
# REFRESH TOKEN FLOW (Meilleure UX)
# ============================================================================

# PROBLÈME: Access token expire vite (30 min)
# -> Utilisateur doit se reconnecter souvent

# SOLUTION: Refresh tokens
# 
# FLOW:
# 1. Login -> Access token (30 min) + Refresh token (7 jours)
# 2. Client stocke les deux (localStorage, secure cookie)
# 3. Utilise access token pour requêtes
# 4. Quand access token expire:
#    - Client envoie refresh token à /refresh
#    - Reçoit nouveau access token
#    - Continue sans re-login!
# 5. Quand refresh token expire -> Vraiment se reconnecter

# CLIENT JAVASCRIPT EXEMPLE:
"""
// Stocker tokens après login
localStorage.setItem('access_token', data.access_token);
localStorage.setItem('refresh_token', data.refresh_token);

// Fonction pour faire requête authentifiée
async function apiRequest(url, options = {}) {
    let token = localStorage.getItem('access_token');
    
    // Ajouter token au header
    options.headers = {
        ...options.headers,
        'Authorization': `Bearer ${token}`
    };
    
    let response = await fetch(url, options);
    
    // Si 401 (token expiré), essayer refresh
    if (response.status === 401) {
        const refreshToken = localStorage.getItem('refresh_token');
        
        // Appeler /refresh
        const refreshResponse = await fetch('/refresh', {
            method: 'POST',
            headers: {
                'Authorization': `Bearer ${refreshToken}`
            }
        });
        
        if (refreshResponse.ok) {
            const data = await refreshResponse.json();
            // Stocker nouveau token
            localStorage.setItem('access_token', data.access_token);
            
            // Réessayer la requête originale
            options.headers.Authorization = `Bearer ${data.access_token}`;
            response = await fetch(url, options);
        } else {
            // Refresh échoué -> Rediriger vers login
            window.location.href = '/login';
        }
    }
    
    return response;
}

// Utilisation:
const response = await apiRequest('/orders', {
    method: 'POST',
    body: JSON.stringify({items: [...]})
});
"""

# ============================================================================
# SÉCURITÉ JWT - BONNES PRATIQUES
# ============================================================================

# [OK] SECRET KEY:
# - Minimum 32 caractères
# - Aléatoire et imprévisible
# - Stocker dans variables d'env (jamais dans code!)
# - Différent par environnement (dev/staging/prod)

# [OK] EXPIRATION:
# - Access token: Court (15-30 min)
# - Refresh token: Long (7-30 jours)
# - Toujours vérifier "exp" claim

# [OK] HTTPS OBLIGATOIRE:
# - JWT transmis en clair (encodé, pas chiffré)
# - HTTPS empêche interception (man-in-the-middle)

# [OK] STOCKAGE CLIENT:
# - localStorage: OK mais vulnérable XSS
# - sessionStorage: Mieux (effacé à fermeture)
# - HttpOnly Cookie: MEILLEUR (pas accessible JavaScript)

# [OK] REFRESH TOKEN ROTATION:
# - À chaque refresh, générer NOUVEAU refresh token
# - Invalider l'ancien
# - Détecte réutilisation (attaque)

# [OK] TOKEN BLACKLIST:
# - Stocker tokens révoqués (Redis)
# - Vérifier blacklist avant d'accepter token
# - Utile pour logout, changement password

# [OK] NE PAS mettre dans JWT:
# - Passwords
# - Numéros de carte bancaire
# - Données personnelles sensibles
# - Données qui changent souvent

# [OK] METTRE dans JWT:
# - User ID
# - Email
# - Role/permissions
# - Données read-only

# ============================================================================
# RÉSUMÉ JWT
# ============================================================================

# WORKFLOW COMPLET:
# 1. User -> POST /register (email, password)
# 2. Server -> Hash password -> Stocker en DB
# 3. User -> POST /login (email, password)
# 4. Server -> Vérifier password -> Générer JWT -> Retourner
# 5. Client -> Stocker JWT
# 6. Client -> Requêtes avec Authorization: Bearer <JWT>
# 7. Server -> Vérifier JWT -> Extraire user info -> Traiter requête
# 8. JWT expire -> Client -> POST /refresh
# 9. Server -> Vérifier refresh token -> Nouveau access token
# 10. Refresh expire -> Client -> POST /login (vraiment se reconnecter)

# AVANTAGES JWT:
# [OK] Stateless (pas besoin session server)
# [OK] Scalable (parfait pour microservices)
# [OK] Auto-suffisant (toutes infos dans token)
# [OK] Standard (implémentations partout)

# INCONVÉNIENTS JWT:
# [X] Impossible de révoquer (sauf blacklist)
# [X] Taille (plus gros qu'un session ID)
# [X] Pas de mise à jour (données figées jusqu'à expiration)


[OK] BASES DE DONNÉES - POSTGRESQL & MONGODB

# ============================================================================
# DATABASE PER SERVICE PATTERN
# ============================================================================

# RÈGLE D'OR MICROSERVICES:
# Chaque service a SA PROPRE base de données!
# 
# [X] MAUVAIS: Base de données partagée
# ┌─────────────┐  ┌─────────────┐
# │   Users     │  │   Orders    │
# │  Service    │  │   Service   │
# └─────┬───────┘  └─────┬───────┘
#       │                │
#       └────────┬───────┘
#         ┌──────[BLACK_DOWN-POINTING_TRIANGLE]──────┐
#         │  DB Shared  │
#         └─────────────┘
# Problèmes: Couplage fort, schema partagé, scaling difficile
#
# [OK] BON: Base de données par service
# ┌─────────────┐  ┌─────────────┐
# │   Users     │  │   Orders    │
# │  Service    │  │   Service   │
# └─────┬───────┘  └─────┬───────┘
#       │                │
#  ┌────[BLACK_DOWN-POINTING_TRIANGLE]────┐      ┌───[BLACK_DOWN-POINTING_TRIANGLE]────┐
#  │ Users DB│      │Orders DB│
#  └─────────┘      └────────┘
# Avantages: Indépendance, scaling séparé, technologies différentes

# POURQUOI?
# [OK] Indépendance: Changer schema Users sans impacter Orders
# [OK] Technologies adaptées: PostgreSQL pour Users, MongoDB pour Products
# [OK] Scaling séparé: Scaler DB Orders sans toucher Users
# [OK] Isolation pannes: Si DB Users tombe, Orders continue

# MAIS:
# [X] Pas de JOIN entre services
# [X] Transactions distribuées complexes (SAGA pattern)
# [X] Duplication données possible

# ============================================================================
# POSTGRESQL AVEC SQLALCHEMY (ASYNC)
# ============================================================================

pip install sqlalchemy asyncpg alembic

# POURQUOI PostgreSQL?
# [OK] Relationnel (tables, foreign keys, constraints)
# [OK] ACID complet (transactions fiables)
# [OK] Mature et stable
# [OK] JSON support (JSONB)
# [OK] Excellent pour données structurées

# POURQUOI SQLAlchemy?
# [OK] ORM Python le plus populaire
# [OK] Support async/await
# [OK] Protection SQL injection
# [OK] Migrations avec Alembic

# ============================================================================
# CONFIGURATION DATABASE
# ============================================================================

# users_service/database.py

from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey
from sqlalchemy.orm import relationship
from datetime import datetime

# URL DE CONNEXION
# Format: postgresql+asyncpg://user:password@host:port/database
DATABASE_URL = "postgresql+asyncpg://admin:secret@localhost:5432/users_db"

# CRÉER ENGINE (moteur de connexion)
engine = create_async_engine(
    DATABASE_URL,
    echo=True,  # Log SQL queries (utile en dev, False en prod)
    
    # POOL DE CONNEXIONS (important!)
    pool_size=20,           # Nombre de connexions maintenues ouvertes
    max_overflow=10,        # Connexions supplémentaires si nécessaire
    pool_pre_ping=True,     # Vérifier connexions avant utilisation
    pool_recycle=3600,      # Recycler connexions après 1h
)

# CRÉER SESSION FACTORY
# Session = "transaction" avec la DB
async_session = sessionmaker(
    engine,
    class_=AsyncSession,
    expire_on_commit=False  # Garder objets après commit
)

# BASE pour tous les modèles
Base = declarative_base()

# ============================================================================
# DÉFINIR MODÈLES (TABLES)
# ============================================================================

class User(Base):
    """
    Modèle User (table users)
    
    Correspond à la table SQL:
    CREATE TABLE users (
        id SERIAL PRIMARY KEY,
        email VARCHAR(255) UNIQUE NOT NULL,
        name VARCHAR(255) NOT NULL,
        password_hash VARCHAR(255) NOT NULL,
        active BOOLEAN DEFAULT TRUE,
        created_at TIMESTAMP DEFAULT NOW()
    );
    """
    __tablename__ = "users"  # Nom de la table
    
    # COLONNES
    id = Column(Integer, primary_key=True, index=True)
    email = Column(String(255), unique=True, index=True, nullable=False)
    name = Column(String(255), nullable=False)
    password_hash = Column(String(255), nullable=False)
    active = Column(Boolean, default=True)
    created_at = Column(DateTime, default=datetime.utcnow)
    
    # RELATIONSHIP: Lien vers table addresses
    addresses = relationship("Address", back_populates="user", cascade="all, delete-orphan")
    
    def __repr__(self):
        return f"<User(id={self.id}, email='{self.email}')>"

class Address(Base):
    """Modèle Address (relation One-to-Many avec User)"""
    __tablename__ = "addresses"
    
    id = Column(Integer, primary_key=True, index=True)
    user_id = Column(Integer, ForeignKey("users.id"), nullable=False)
    street = Column(String(255))
    city = Column(String(100))
    country = Column(String(100))
    is_default = Column(Boolean, default=False)
    
    # RELATIONSHIP inverse
    user = relationship("User", back_populates="addresses")

# ============================================================================
# DÉPENDANCE FASTAPI POUR SESSION DB
# ============================================================================

async def get_db() -> AsyncSession:
    """
    Dépendance FastAPI pour obtenir session DB
    
    Usage:
    @app.get("/users")
    async def list_users(db: AsyncSession = Depends(get_db)):
        ...
    
    Gère automatiquement:
    - Ouverture session
    - Fermeture session (même si erreur)
    """
    async with async_session() as session:
        try:
            yield session  # Fournir session à la route
        finally:
            await session.close()  # Toujours fermer

# ============================================================================
# CRÉER LES TABLES
# ============================================================================

async def init_db():
    """
    Créer toutes les tables dans la DB
    
    À exécuter au démarrage de l'app (une fois)
    """
    async with engine.begin() as conn:
        # Créer TOUTES les tables définies (User, Address, etc.)
        await conn.run_sync(Base.metadata.create_all)
        print("[OK] Database tables created")

# Dans main.py:
from fastapi import FastAPI
from database import init_db

app = FastAPI()

@app.on_event("startup")
async def startup_event():
    """Exécuté au démarrage de l'app"""
    await init_db()

# ============================================================================
# OPÉRATIONS CRUD (CREATE, READ, UPDATE, DELETE)
# ============================================================================

from fastapi import Depends
from sqlalchemy import select, update, delete
from sqlalchemy.orm import selectinload

# ========== CREATE ==========

@app.post("/users")
async def create_user(
    email: str,
    name: str,
    password: str,
    db: AsyncSession = Depends(get_db)
):
    """
    Créer un utilisateur
    
    SQLAlchemy génère automatiquement:
    INSERT INTO users (email, name, password_hash, active, created_at)
    VALUES ('john@example.com', 'John', '...', true, NOW())
    RETURNING id;
    """
    
    # ÉTAPE 1: Créer instance du modèle
    new_user = User(
        email=email,
        name=name,
        password_hash=hash_password(password),  # Hash le password!
        active=True
    )
    
    # ÉTAPE 2: Ajouter à la session
    db.add(new_user)
    
    # ÉTAPE 3: Commit (sauvegarder en DB)
    await db.commit()
    
    # ÉTAPE 4: Refresh pour obtenir l'ID généré
    await db.refresh(new_user)
    
    return {
        "id": new_user.id,
        "email": new_user.email,
        "name": new_user.name,
        "created_at": new_user.created_at
    }

# ========== READ (SELECT) ==========

@app.get("/users/{user_id}")
async def get_user(user_id: int, db: AsyncSession = Depends(get_db)):
    """
    Récupérer un utilisateur par ID
    
    SQL généré:
    SELECT * FROM users WHERE id = 123;
    """
    
    # Méthode 1: get() - Simple
    result = await db.get(User, user_id)
    
    if not result:
        raise HTTPException(status_code=404, detail="User not found")
    
    return result

@app.get("/users")
async def list_users(
    skip: int = 0,
    limit: int = 10,
    db: AsyncSession = Depends(get_db)
):
    """
    Lister utilisateurs avec pagination
    
    SQL généré:
    SELECT * FROM users LIMIT 10 OFFSET 0;
    """
    
    # Méthode 2: execute() avec select() - Plus flexible
    stmt = select(User).offset(skip).limit(limit)
    result = await db.execute(stmt)
    users = result.scalars().all()
    
    return users

@app.get("/users/{user_id}/addresses")
async def get_user_with_addresses(
    user_id: int,
    db: AsyncSession = Depends(get_db)
):
    """
    Récupérer user avec ses adresses (éviter N+1 queries)
    
    SQL généré (1 seule query avec JOIN):
    SELECT users.*, addresses.*
    FROM users
    LEFT JOIN addresses ON users.id = addresses.user_id
    WHERE users.id = 123;
    """
    
    # EAGER LOADING: Charger user ET addresses en 1 query
    stmt = select(User).options(
        selectinload(User.addresses)  # Charger addresses aussi
    ).where(User.id == user_id)
    
    result = await db.execute(stmt)
    user = result.scalar_one_or_none()
    
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    
    return {
        "id": user.id,
        "email": user.email,
        "name": user.name,
        "addresses": [
            {
                "id": addr.id,
                "street": addr.street,
                "city": addr.city,
                "country": addr.country
            }
            for addr in user.addresses
        ]
    }

# ========== UPDATE ==========

@app.put("/users/{user_id}")
async def update_user(
    user_id: int,
    name: str = None,
    email: str = None,
    db: AsyncSession = Depends(get_db)
):
    """
    Mettre à jour un utilisateur
    
    SQL généré:
    UPDATE users
    SET name = 'New Name', email = 'new@example.com'
    WHERE id = 123;
    """
    
    # Méthode 1: Récupérer, modifier, commit
    user = await db.get(User, user_id)
    
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    
    if name:
        user.name = name
    if email:
        user.email = email
    
    await db.commit()
    await db.refresh(user)
    
    return user

# Méthode 2: UPDATE direct (plus efficace)
@app.put("/users/{user_id}/activate")
async def activate_user(user_id: int, db: AsyncSession = Depends(get_db)):
    """
    Activer utilisateur (UPDATE direct)
    """
    stmt = (
        update(User)
        .where(User.id == user_id)
        .values(active=True)
    )
    
    result = await db.execute(stmt)
    await db.commit()
    
    if result.rowcount == 0:
        raise HTTPException(status_code=404, detail="User not found")
    
    return {"message": "User activated"}

# ========== DELETE ==========

@app.delete("/users/{user_id}")
async def delete_user(user_id: int, db: AsyncSession = Depends(get_db)):
    """
    Supprimer un utilisateur
    
    SQL généré:
    DELETE FROM users WHERE id = 123;
    
    CASCADE: Supprime aussi les addresses automatiquement!
    """
    
    # Méthode 1: Récupérer puis delete
    user = await db.get(User, user_id)
    
    if not user:
        raise HTTPException(status_code=404, detail="User not found")
    
    await db.delete(user)
    await db.commit()
    
    return {"message": "User deleted"}

# Méthode 2: DELETE direct
@app.delete("/users/bulk")
async def delete_inactive_users(db: AsyncSession = Depends(get_db)):
    """
    Supprimer tous les utilisateurs inactifs
    """
    stmt = delete(User).where(User.active == False)
    result = await db.execute(stmt)
    await db.commit()
    
    return {"deleted": result.rowcount}

# ============================================================================
# REQUÊTES AVANCÉES
# ============================================================================

from sqlalchemy import and_, or_, func

@app.get("/users/search")
async def search_users(
    name: str = None,
    email: str = None,
    active: bool = None,
    db: AsyncSession = Depends(get_db)
):
    """
    Recherche avancée avec filtres multiples
    
    SQL généré:
    SELECT * FROM users
    WHERE name ILIKE '%john%'
      AND email ILIKE '%example.com'
      AND active = true;
    """
    
    # Construire query dynamiquement
    stmt = select(User)
    
    # Ajouter filtres si fournis
    conditions = []
    
    if name:
        # ILIKE = insensible à la casse
        conditions.append(User.name.ilike(f"%{name}%"))
    
    if email:
        conditions.append(User.email.ilike(f"%{email}%"))
    
    if active is not None:
        conditions.append(User.active == active)
    
    # Combiner conditions avec AND
    if conditions:
        stmt = stmt.where(and_(*conditions))
    
    result = await db.execute(stmt)
    users = result.scalars().all()
    
    return users

@app.get("/stats/users")
async def get_user_stats(db: AsyncSession = Depends(get_db)):
    """
    Statistiques avec COUNT, AVG, etc.
    
    SQL généré:
    SELECT
        COUNT(*) as total,
        COUNT(*) FILTER (WHERE active = true) as active_count,
        COUNT(*) FILTER (WHERE active = false) as inactive_count
    FROM users;
    """
    
    stmt = select(
        func.count(User.id).label("total"),
        func.count(User.id).filter(User.active == True).label("active"),
        func.count(User.id).filter(User.active == False).label("inactive")
    )
    
    result = await db.execute(stmt)
    stats = result.first()
    
    return {
        "total_users": stats.total,
        "active_users": stats.active,
        "inactive_users": stats.inactive
    }

# ============================================================================
# TRANSACTIONS
# ============================================================================

from sqlalchemy.exc import IntegrityError

@app.post("/users/batch")
async def create_users_batch(
    users: list[dict],
    db: AsyncSession = Depends(get_db)
):
    """
    Créer plusieurs utilisateurs en transaction
    
    Transaction = TOUT ou RIEN
    Si 1 user échoue -> TOUT est annulé (rollback)
    """
    
    try:
        # Tout dans une transaction
        for user_data in users:
            new_user = User(
                email=user_data["email"],
                name=user_data["name"],
                password_hash=hash_password(user_data["password"])
            )
            db.add(new_user)
        
        # Commit TOUS les users en même temps
        await db.commit()
        
        return {"created": len(users)}
    
    except IntegrityError as e:
        # Erreur (email duplicate, constraint violation)
        await db.rollback()  # Annuler TOUT
        raise HTTPException(
            status_code=400,
            detail=f"Database error: {str(e)}"
        )

# ============================================================================
# ALEMBIC - MIGRATIONS DE SCHÉMA
# ============================================================================

# PROBLÈME: Vous modifiez un modèle (ajout colonne, etc.)
# Comment mettre à jour la DB en production sans tout casser?
#
# SOLUTION: ALEMBIC = Outil de migrations
# Comme "git" mais pour le schéma de votre DB

# INSTALLATION
pip install alembic

# INITIALISER ALEMBIC
alembic init alembic

# Crée:
# alembic/
#   env.py           # Configuration
#   script.py.mako   # Template migrations
#   versions/        # Dossier migrations
# alembic.ini        # Config principale

# CONFIGURATION (alembic.ini)
# Modifier la ligne:
# sqlalchemy.url = postgresql://admin:secret@localhost:5432/users_db

# OU mieux, dans alembic/env.py:
from database import DATABASE_URL
config.set_main_option("sqlalchemy.url", DATABASE_URL)

# CRÉER UNE MIGRATION (automatique)
alembic revision --autogenerate -m "Add phone column to users"

# Alembic compare les modèles vs DB actuelle
# Génère automatiquement le code de migration!

# Fichier créé: alembic/versions/abc123_add_phone_column.py
"""Add phone column to users

Revision ID: abc123
Revises: xyz789
Create Date: 2024-01-15 10:30:00
"""

def upgrade():
    """Migration vers le haut (appliquer changement)"""
    op.add_column('users', sa.Column('phone', sa.String(20), nullable=True))

def downgrade():
    """Migration vers le bas (annuler changement)"""
    op.drop_column('users', 'phone')

# APPLIQUER LA MIGRATION
alembic upgrade head

# Alembic exécute:
# ALTER TABLE users ADD COLUMN phone VARCHAR(20);

# ANNULER LA DERNIÈRE MIGRATION
alembic downgrade -1

# VOIR HISTORIQUE
alembic history

# VOIR RÉVISION ACTUELLE
alembic current

# EXEMPLES DE MIGRATIONS COURANTES

# Migration 1: Ajouter colonne
def upgrade():
    op.add_column('users', sa.Column('last_login', sa.DateTime(), nullable=True))

# Migration 2: Modifier colonne
def upgrade():
    op.alter_column('users', 'name', existing_type=sa.String(100), type_=sa.String(255))

# Migration 3: Ajouter index
def upgrade():
    op.create_index('idx_users_email', 'users', ['email'])

# Migration 4: Créer nouvelle table
def upgrade():
    op.create_table(
        'user_sessions',
        sa.Column('id', sa.Integer(), primary_key=True),
        sa.Column('user_id', sa.Integer(), sa.ForeignKey('users.id')),
        sa.Column('token', sa.String(255)),
        sa.Column('expires_at', sa.DateTime())
    )

# Migration 5: Data migration (modifier données)
def upgrade():
    # Mettre tous les users inactifs à actifs
    op.execute("UPDATE users SET active = true WHERE active IS NULL")

# ============================================================================
# MONGODB AVEC MOTOR (ASYNC)
# ============================================================================

pip install motor beanie

# POURQUOI MongoDB?
# [OK] NoSQL (pas de schéma fixe)
# [OK] Documents JSON (flexible)
# [OK] Très scalable horizontalement
# [OK] Excellentpour données non-structurées
# [OK] Embedded documents (pas de JOINs)

# QUAND UTILISER MongoDB?
# [OK] Schéma changeant fréquemment
# [OK] Données hiérarchiques (embedded docs)
# [OK] Logs, events, analytics
# [OK] Catalogue produits (attributs variables)

# CONFIGURATION

from motor.motor_asyncio import AsyncIOMotorClient
from beanie import Document, init_beanie
from pydantic import EmailStr
from typing import Optional, List
from datetime import datetime

# URL MongoDB
MONGODB_URL = "mongodb://localhost:27017"
DATABASE_NAME = "products_db"

# Client MongoDB
mongo_client = AsyncIOMotorClient(MONGODB_URL)
database = mongo_client[DATABASE_NAME]

# DÉFINIR MODÈLE (Document)

class Product(Document):
    """
    Modèle Product pour MongoDB
    
    Document MongoDB:
    {
        "_id": ObjectId("..."),
        "name": "iPhone 15",
        "description": "Latest iPhone",
        "price": 999.99,
        "category": "Electronics",
        "stock": 50,
        "attributes": {
            "color": "Black",
            "storage": "256GB"
        },
        "tags": ["smartphone", "apple"],
        "created_at": ISODate("...")
    }
    """
    
    name: str
    description: Optional[str] = None
    price: float
    category: str
    stock: int = 0
    attributes: dict = {}  # Flexible! Peut contenir n'importe quoi
    tags: List[str] = []
    created_at: datetime = datetime.utcnow()
    
    class Settings:
        name = "products"  # Nom de la collection
        indexes = [
            "name",        # Index sur name
            "category",    # Index sur category
            [("price", 1), ("stock", -1)]  # Index composé
        ]

# INITIALISER BEANIE
@app.on_event("startup")
async def startup_db():
    """Initialiser Beanie au démarrage"""
    await init_beanie(
        database=database,
        document_models=[Product]  # Liste de tous vos modèles
    )
    print("[OK] MongoDB connected")

# OPÉRATIONS CRUD MONGODB

# ========== CREATE ==========

@app.post("/products")
async def create_product(
    name: str,
    price: float,
    category: str,
    attributes: dict = {}
):
    """
    Créer un produit
    
    MongoDB:
    db.products.insertOne({
        name: "iPhone 15",
        price: 999.99,
        ...
    })
    """
    
    product = Product(
        name=name,
        price=price,
        category=category,
        attributes=attributes
    )
    
    await product.insert()  # Sauvegarder dans MongoDB
    
    return product

# ========== READ ==========

@app.get("/products/{product_id}")
async def get_product(product_id: str):
    """
    Récupérer produit par ID
    
    MongoDB:
    db.products.findOne({_id: ObjectId("...")})
    """
    
    product = await Product.get(product_id)
    
    if not product:
        raise HTTPException(status_code=404, detail="Product not found")
    
    return product

@app.get("/products")
async def list_products(
    category: str = None,
    min_price: float = None,
    max_price: float = None,
    skip: int = 0,
    limit: int = 10
):
    """
    Lister produits avec filtres
    
    MongoDB:
    db.products.find({
        category: "Electronics",
        price: {$gte: 100, $lte: 1000}
    }).skip(0).limit(10)
    """
    
    # Construire query
    query = {}
    
    if category:
        query["category"] = category
    
    if min_price or max_price:
        query["price"] = {}
        if min_price:
            query["price"]["$gte"] = min_price  # Greater Than or Equal
        if max_price:
            query["price"]["$lte"] = max_price  # Less Than or Equal
    
    # Exécuter query
    products = await Product.find(query).skip(skip).limit(limit).to_list()
    
    return products

# ========== UPDATE ==========

@app.put("/products/{product_id}")
async def update_product(
    product_id: str,
    price: float = None,
    stock: int = None,
    attributes: dict = None
):
    """
    Mettre à jour produit
    
    MongoDB:
    db.products.updateOne(
        {_id: ObjectId("...")},
        {$set: {price: 899.99, stock: 30}}
    )
    """
    
    product = await Product.get(product_id)
    
    if not product:
        raise HTTPException(status_code=404, detail="Product not found")
    
    if price is not None:
        product.price = price
    if stock is not None:
        product.stock = stock
    if attributes:
        product.attributes.update(attributes)  # Merge attributes
    
    await product.save()  # Sauvegarder changements
    
    return product

# ========== DELETE ==========

@app.delete("/products/{product_id}")
async def delete_product(product_id: str):
    """
    Supprimer produit
    
    MongoDB:
    db.products.deleteOne({_id: ObjectId("...")})
    """
    
    product = await Product.get(product_id)
    
    if not product:
        raise HTTPException(status_code=404, detail="Product not found")
    
    await product.delete()
    
    return {"message": "Product deleted"}

# REQUÊTES AVANCÉES MONGODB

@app.get("/products/search")
async def search_products(query: str):
    """
    Recherche full-text
    
    MongoDB:
    db.products.find({$text: {$search: "iphone"}})
    """
    
    products = await Product.find(
        {"$text": {"$search": query}}
    ).to_list()
    
    return products

@app.get("/products/category/{category}/stats")
async def category_stats(category: str):
    """
    Aggregation: statistiques par catégorie
    
    MongoDB Aggregation Pipeline:
    db.products.aggregate([
        {$match: {category: "Electronics"}},
        {$group: {
            _id: null,
            avg_price: {$avg: "$price"},
            total_stock: {$sum: "$stock"},
            count: {$sum: 1}
        }}
    ])
    """
    
    pipeline = [
        {"$match": {"category": category}},
        {"$group": {
            "_id": None,
            "avg_price": {"$avg": "$price"},
            "total_stock": {"$sum": "$stock"},
            "count": {"$sum": 1},
            "min_price": {"$min": "$price"},
            "max_price": {"$max": "$price"}
        }}
    ]
    
    result = await Product.aggregate(pipeline).to_list()
    
    if not result:
        return {"category": category, "count": 0}
    
    return {
        "category": category,
        **result[0]
    }

# ============================================================================
# RÉSUMÉ BASES DE DONNÉES
# ============================================================================

# POSTGRESQL + SQLAlchemy:
# [OK] Données structurées, relationnelles
# [OK] Transactions ACID
# [OK] Foreign keys, constraints
# [OK] Migrations avec Alembic
# Exemples: Users, Orders, Invoices

# MONGODB + Motor/Beanie:
# [OK] Données non-structurées, flexibles
# [OK] Schéma changeant
# [OK] Embedded documents
# [OK] Très scalable
# Exemples: Products (attributs variables), Logs, Events

# CHOIX:
# - Users, Orders, Payments -> PostgreSQL
# - Products, Reviews, Analytics -> MongoDB
# - Logs, Events, Sessions -> MongoDB ou Redis


[OK] MONITORING & OBSERVABILITÉ

# ============================================================================
# LES 3 PILIERS DE L'OBSERVABILITÉ
# ============================================================================

# En microservices, vous DEVEZ savoir ce qui se passe!
# 10+ services -> impossible de débugger manuellement

# 1. METRICS (Métriques)
#    Chiffres: CPU, RAM, requêtes/sec, temps réponse, erreurs
#    Outil: Prometheus + Grafana
#
# 2. LOGS (Journaux)
#    Messages: "User 123 logged in", "Error in payment service"
#    Outil: ELK Stack (Elasticsearch, Logstash, Kibana)
#
# 3. TRACES (Traçabilité)
#    Suivi requête à travers services: API Gateway -> Users -> Orders -> Payment
#    Outil: Jaeger, Zipkin

# ANALOGIE:
# Metrics = Tableau de bord de voiture (vitesse, essence, température)
# Logs = Carnet de bord détaillé ("10h: démarré", "10h15: arrêt station")
# Traces = GPS avec historique complet du trajet

# ============================================================================
# PROMETHEUS - COLLECTE DE MÉTRIQUES
# ============================================================================

pip install prometheus-client prometheus-fastapi-instrumentator

# POURQUOI Prometheus?
# [OK] Standard de facto pour métriques
# [OK] Time-series database (données horodatées)
# [OK] Pull model (Prometheus récupère les métriques)
# [OK] PromQL (langage de requête puissant)
# [OK] Alerting intégré

# TYPES DE MÉTRIQUES:

# 1. COUNTER: Ne fait qu'augmenter
#    Exemples: Total requêtes, total erreurs
#
# 2. GAUGE: Peut monter et descendre
#    Exemples: CPU actuel, RAM actuelle, utilisateurs connectés
#
# 3. HISTOGRAM: Distribution de valeurs
#    Exemples: Temps de réponse (combien en <100ms, 100-200ms, >200ms)
#
# 4. SUMMARY: Comme histogram mais avec percentiles
#    Exemples: Temps réponse P50, P95, P99

# INSTRUMENTER FASTAPI AVEC PROMETHEUS

from prometheus_client import Counter, Histogram, Gauge, generate_latest
from prometheus_fastapi_instrumentator import Instrumentator
from fastapi import FastAPI, Response

app = FastAPI(title="Users Service")

# ========== MÉTRIQUES PERSONNALISÉES ==========

# COUNTER: Total de requêtes par endpoint et status
request_count = Counter(
    'http_requests_total',           # Nom métrique
    'Total HTTP requests',            # Description
    ['method', 'endpoint', 'status']  # Labels (dimensions)
)

# Exemple d'utilisation:
# request_count.labels(method="GET", endpoint="/users", status="200").inc()
# -> Incrémente compteur pour GET /users avec status 200

# HISTOGRAM: Temps de réponse des requêtes
request_duration = Histogram(
    'http_request_duration_seconds',
    'HTTP request duration in seconds',
    ['method', 'endpoint'],
    buckets=[0.01, 0.05, 0.1, 0.5, 1.0, 2.0, 5.0]  # Buckets pour distribution
)

# GAUGE: Nombre d'utilisateurs actifs
active_users = Gauge(
    'active_users_total',
    'Number of currently active users'
)

# GAUGE: Nombre de connexions DB actives
db_connections = Gauge(
    'database_connections_active',
    'Number of active database connections'
)

# ========== INSTRUMENTER AUTOMATIQUEMENT ==========

# Prometheus FastAPI Instrumentator = Métriques automatiques!
Instrumentator().instrument(app).expose(app)

# Ajoute automatiquement:
# - http_requests_total
# - http_request_duration_seconds
# - http_requests_in_progress
# Et expose endpoint /metrics

# ========== INSTRUMENTER MANUELLEMENT ==========

import time
from fastapi import Request

@app.middleware("http")
async def prometheus_middleware(request: Request, call_next):
    """
    Middleware pour tracker métriques personnalisées
    """
    
    # Démarrer timer
    start_time = time.time()
    
    # Exécuter requête
    response = await call_next(request)
    
    # Calculer durée
    duration = time.time() - start_time
    
    # Enregistrer métriques
    request_count.labels(
        method=request.method,
        endpoint=request.url.path,
        status=response.status_code
    ).inc()
    
    request_duration.labels(
        method=request.method,
        endpoint=request.url.path
    ).observe(duration)
    
    return response

# ========== MÉTRIQUES MÉTIER ==========

# Métriques spécifiques à votre domaine

# Orders Service
orders_created = Counter('orders_created_total', 'Total orders created')
order_value = Histogram('order_value_dollars', 'Order value in dollars')
order_items = Histogram('order_items_count', 'Number of items per order')

@app.post("/orders")
async def create_order(order: OrderCreate):
    # ... logique création commande ...
    
    # Incrémenter compteur
    orders_created.inc()
    
    # Enregistrer valeur commande
    total = sum(item.price * item.quantity for item in order.items)
    order_value.observe(total)
    
    # Enregistrer nombre d'items
    order_items.observe(len(order.items))
    
    return new_order

# Users Service
user_registrations = Counter('user_registrations_total', 'Total user registrations')
user_logins = Counter('user_logins_total', 'Total user logins', ['success'])
active_sessions = Gauge('active_sessions_total', 'Number of active user sessions')

@app.post("/register")
async def register(user: UserRegister):
    # ... création user ...
    user_registrations.inc()
    return new_user

@app.post("/login")
async def login(credentials: UserLogin):
    if verify_password(...):
        user_logins.labels(success="true").inc()
        active_sessions.inc()  # Nouvelle session
        return token
    else:
        user_logins.labels(success="false").inc()
        raise HTTPException(401)

# ========== ENDPOINT MÉTRIQUES ==========

# Déjà exposé par Instrumentator sur /metrics
# Ou manuellement:

@app.get("/metrics")
async def metrics():
    """
    Endpoint pour Prometheus scraping
    
    Format texte spécial Prometheus:
    # HELP http_requests_total Total HTTP requests
    # TYPE http_requests_total counter
    http_requests_total{method="GET",endpoint="/users",status="200"} 1543
    http_requests_total{method="POST",endpoint="/users",status="201"} 87
    """
    return Response(
        content=generate_latest(),
        media_type="text/plain"
    )

# ========== CONFIGURATION PROMETHEUS ==========

# Fichier: prometheus.yml
global:
  scrape_interval: 15s  # Récupérer métriques toutes les 15s
  evaluation_interval: 15s

scrape_configs:
  # Job 1: Users Service
  - job_name: 'users-service'
    static_configs:
      - targets: ['users-service:8001']  # URL du service
        labels:
          service: 'users'
          environment: 'production'
  
  # Job 2: Orders Service
  - job_name: 'orders-service'
    static_configs:
      - targets: ['orders-service:8002']
        labels:
          service: 'orders'
          environment: 'production'
  
  # Job 3: Products Service
  - job_name: 'products-service'
    static_configs:
      - targets: ['products-service:8003']
        labels:
          service: 'products'
          environment: 'production'

# LANCER PROMETHEUS (Docker)
docker run -d \
  --name prometheus \
  -p 9090:9090 \
  -v $(pwd)/prometheus.yml:/etc/prometheus/prometheus.yml \
  prom/prometheus

# Interface web: http://localhost:9090

# ========== REQUÊTES PROMQL ==========

# PromQL = Langage de requête Prometheus

# Requête 1: Taux de requêtes par seconde (dernière minute)
rate(http_requests_total[1m])

# Requête 2: Latence moyenne par endpoint (5 dernières minutes)
rate(http_request_duration_seconds_sum[5m]) / rate(http_request_duration_seconds_count[5m])

# Requête 3: Taux d'erreurs (status 5xx)
rate(http_requests_total{status=~"5.."}[5m])

# Requête 4: Nombre d'utilisateurs actifs par service
sum by (service) (active_users_total)

# Requête 5: P95 temps de réponse
histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m]))

# Requête 6: Top 5 endpoints les plus lents
topk(5, rate(http_request_duration_seconds_sum[5m]))

# ============================================================================
# GRAFANA - VISUALISATION DES MÉTRIQUES
# ============================================================================

# Grafana = Interface graphique pour visualiser Prometheus

# LANCER GRAFANA (Docker)
docker run -d \
  --name grafana \
  -p 3000:3000 \
  -e "GF_SECURITY_ADMIN_PASSWORD=admin" \
  grafana/grafana

# Interface web: http://localhost:3000
# Login: admin / admin

# ========== CONFIGURER DATASOURCE PROMETHEUS ==========

# 1. Configuration -> Data Sources -> Add data source
# 2. Sélectionner "Prometheus"
# 3. URL: http://prometheus:9090 (si Docker) ou http://localhost:9090
# 4. Save & Test

# ========== CRÉER UN DASHBOARD ==========

# Dashboard JSON exemple: microservices_dashboard.json
{
  "dashboard": {
    "title": "Microservices Overview",
    "panels": [
      {
        "title": "Request Rate (req/s)",
        "targets": [{
          "expr": "rate(http_requests_total[1m])"
        }],
        "type": "graph"
      },
      {
        "title": "Error Rate (%)",
        "targets": [{
          "expr": "rate(http_requests_total{status=~\"5..\"}[5m]) / rate(http_requests_total[5m]) * 100"
        }],
        "type": "graph"
      },
      {
        "title": "Latency P95 (ms)",
        "targets": [{
          "expr": "histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) * 1000"
        }],
        "type": "graph"
      },
      {
        "title": "Active Users",
        "targets": [{
          "expr": "active_users_total"
        }],
        "type": "stat"
      }
    ]
  }
}

# Importer: Dashboards -> Import -> Paste JSON

# ========== DASHBOARDS PRÉDÉFINIS ==========

# Grafana a des dashboards communautaires:
# https://grafana.com/grafana/dashboards/

# Populaires pour microservices:
# - 11159: FastAPI Observability
# - 3662: Prometheus 2.0 Overview
# - 1860: Node Exporter Full

# Importer par ID: Dashboard -> Import -> Enter Dashboard ID

# ============================================================================
# ALERTING AVEC PROMETHEUS
# ============================================================================

# Alertes = Notification si problème détecté

# Fichier: alert_rules.yml
groups:
  - name: microservices_alerts
    interval: 30s
    rules:
      
      # Alerte 1: Taux d'erreurs élevé
      - alert: HighErrorRate
        expr: |
          rate(http_requests_total{status=~"5.."}[5m]) / rate(http_requests_total[5m]) > 0.05
        for: 5m
        labels:
          severity: critical
        annotations:
          summary: "High error rate on {{ $labels.service }}"
          description: "Error rate is {{ $value | humanizePercentage }} (threshold: 5%)"
      
      # Alerte 2: Latence élevée
      - alert: HighLatency
        expr: |
          histogram_quantile(0.95, rate(http_request_duration_seconds_bucket[5m])) > 1.0
        for: 10m
        labels:
          severity: warning
        annotations:
          summary: "High latency on {{ $labels.service }}"
          description: "P95 latency is {{ $value }}s (threshold: 1s)"
      
      # Alerte 3: Service down
      - alert: ServiceDown
        expr: up == 0
        for: 1m
        labels:
          severity: critical
        annotations:
          summary: "Service {{ $labels.job }} is down"
          description: "{{ $labels.job }} has been down for more than 1 minute"
      
      # Alerte 4: Utilisation DB élevée
      - alert: HighDBConnections
        expr: database_connections_active > 80
        for: 5m
        labels:
          severity: warning
        annotations:
          summary: "High DB connections on {{ $labels.service }}"
          description: "DB connections: {{ $value }} (threshold: 80)"

# Configurer Prometheus pour charger les alertes:
# prometheus.yml
rule_files:
  - "alert_rules.yml"

# ========== ALERTMANAGER (Envoyer notifications) ==========

# Prometheus détecte -> Alertmanager envoie notifications

# Fichier: alertmanager.yml
global:
  resolve_timeout: 5m

route:
  group_by: ['alertname', 'service']
  group_wait: 10s
  group_interval: 10s
  repeat_interval: 12h
  receiver: 'default'
  
  routes:
    # Alertes critiques -> email + Slack
    - match:
        severity: critical
      receiver: 'critical-alerts'
    
    # Alertes warning -> Slack seulement
    - match:
        severity: warning
      receiver: 'warning-alerts'

receivers:
  # Email
  - name: 'critical-alerts'
    email_configs:
      - to: 'ops-team@example.com'
        from: 'alertmanager@example.com'
        smarthost: smtp.gmail.com:587
        auth_username: 'alertmanager@example.com'
        auth_password: 'password'
  
  # Slack
  - name: 'warning-alerts'
    slack_configs:
      - api_url: 'https://hooks.slack.com/services/YOUR/WEBHOOK/URL'
        channel: '#alerts'
        title: 'Alert: {{ .GroupLabels.alertname }}'
        text: '{{ range .Alerts }}{{ .Annotations.description }}{{ end }}'

# Lancer Alertmanager:
docker run -d \
  --name alertmanager \
  -p 9093:9093 \
  -v $(pwd)/alertmanager.yml:/etc/alertmanager/alertmanager.yml \
  prom/alertmanager

# Configurer Prometheus pour utiliser Alertmanager:
# prometheus.yml
alerting:
  alertmanagers:
    - static_configs:
        - targets: ['alertmanager:9093']

# ============================================================================
# LOGGING CENTRALISÉ
# ============================================================================

# PROBLÈME: 10 services × 3 instances = 30 fichiers de logs!
# SOLUTION: Logging centralisé (tous les logs au même endroit)

pip install python-json-logger structlog

# ========== STRUCTURED LOGGING ==========

# LOG CLASSIQUE (mauvais):
# "User john@example.com created order 123 with total $99.99"
# -> Difficile à parser, rechercher, filtrer

# LOG STRUCTURÉ (bon):
# {"timestamp": "2024-01-15T10:30:00Z", "level": "INFO", "event": "order_created", "user_email": "john@example.com", "order_id": 123, "total": 99.99}
# -> Facile à indexer, rechercher, analyser!

# Configuration structlog
import structlog
from datetime import datetime

structlog.configure(
    processors=[
        structlog.stdlib.filter_by_level,
        structlog.stdlib.add_logger_name,
        structlog.stdlib.add_log_level,
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.processors.UnicodeDecoder(),
        structlog.processors.JSONRenderer()  # <- JSON output!
    ],
    context_class=dict,
    logger_factory=structlog.stdlib.LoggerFactory(),
    cache_logger_on_first_use=True,
)

logger = structlog.get_logger()

# Utilisation
@app.post("/orders")
async def create_order(order: OrderCreate, current_user = Depends(get_current_user)):
    
    # Log structuré
    logger.info(
        "order_create_started",
        user_id=current_user["sub"],
        user_email=current_user["email"],
        items_count=len(order.items)
    )
    
    try:
        # ... créer commande ...
        
        logger.info(
            "order_created",
            order_id=new_order["id"],
            user_id=current_user["sub"],
            total=new_order["total"],
            status=new_order["status"]
        )
        
        return new_order
    
    except Exception as e:
        logger.error(
            "order_creation_failed",
            user_id=current_user["sub"],
            error=str(e),
            exc_info=True  # Inclure stack trace
        )
        raise

# Output:
# {"timestamp": "2024-01-15T10:30:00.123Z", "level": "info", "event": "order_created", "order_id": 123, "user_id": "456", "total": 99.99, "status": "created"}

# ========== CORRELATION ID ==========

# CRITIQUE: Tracer une requête à travers TOUS les services!

# Exemple sans Correlation ID:
# Client -> Gateway -> Users -> Orders -> Payment
# Si erreur dans Payment, comment retrouver logs liés dans les 4 autres services? [!]

# Solution: Correlation ID (UUID unique par requête)

import uuid
from contextvars import ContextVar

# Variable contextuelle (thread-safe, async-safe)
correlation_id_var: ContextVar[str] = ContextVar('correlation_id', default='')

@app.middleware("http")
async def correlation_middleware(request: Request, call_next):
    """
    Générer ou extraire Correlation ID
    """
    
    # Extraire du header (si vient d'un autre service)
    correlation_id = request.headers.get('X-Correlation-ID')
    
    # Sinon, générer nouveau
    if not correlation_id:
        correlation_id = str(uuid.uuid4())
    
    # Stocker dans contexte (accessible partout)
    correlation_id_var.set(correlation_id)
    
    # Ajouter à tous les logs
    with structlog.contextvars.bound_contextvars(correlation_id=correlation_id):
        response = await call_next(request)
    
    # Ajouter au header de réponse
    response.headers['X-Correlation-ID'] = correlation_id
    
    return response

# Propager aux services appelés
async def call_other_service(url: str):
    """Appeler autre service avec Correlation ID"""
    headers = {
        'X-Correlation-ID': correlation_id_var.get()
    }
    
    async with httpx.AsyncClient() as client:
        response = await client.get(url, headers=headers)
        return response.json()

# Maintenant tous les logs ont le même correlation_id!
# {"timestamp": "...", "level": "info", "event": "order_created", "correlation_id": "abc-123-def-456", ...}
# {"timestamp": "...", "level": "info", "event": "payment_processed", "correlation_id": "abc-123-def-456", ...}
# 
# Recherche dans logs: correlation_id="abc-123-def-456"
# -> Voir TOUTE la requête à travers tous les services! [BRAVO]

# ============================================================================
# ELK STACK (Elasticsearch, Logstash, Kibana)
# ============================================================================

# ELK = Solution complète pour logging centralisé

# 1. LOGSTASH: Collecte logs depuis services
# 2. ELASTICSEARCH: Indexe et stocke logs
# 3. KIBANA: Interface pour rechercher et visualiser

# docker-compose.yml pour ELK
version: '3.8'
services:
  
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    environment:
      - discovery.type=single-node
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
      - xpack.security.enabled=false
    ports:
      - "9200:9200"
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data
  
  logstash:
    image: docker.elastic.co/logstash/logstash:8.11.0
    ports:
      - "5959:5959"  # Port TCP pour logs
    volumes:
      - ./logstash.conf:/usr/share/logstash/pipeline/logstash.conf
    depends_on:
      - elasticsearch
  
  kibana:
    image: docker.elastic.co/kibana/kibana:8.11.0
    ports:
      - "5601:5601"
    environment:
      - ELASTICSEARCH_HOSTS=http://elasticsearch:9200
    depends_on:
      - elasticsearch

volumes:
  elasticsearch_data:

# Fichier: logstash.conf
input {
  # Recevoir logs via TCP
  tcp {
    port => 5959
    codec => json
  }
}

filter {
  # Parser timestamp
  date {
    match => [ "timestamp", "ISO8601" ]
  }
  
  # Ajouter tags selon niveau
  if [level] == "error" {
    mutate {
      add_tag => [ "error" ]
    }
  }
}

output {
  # Envoyer vers Elasticsearch
  elasticsearch {
    hosts => ["elasticsearch:9200"]
    index => "microservices-logs-%{+YYYY.MM.dd}"
  }
  
  # Afficher dans console (debug)
  stdout {
    codec => rubydebug
  }
}

# Envoyer logs à Logstash depuis Python
pip install python-logstash

import logstash

logger = logging.getLogger()
logger.addHandler(logstash.TCPLogstashHandler('localhost', 5959, version=1))

logger.info("User created", extra={
    "user_id": 123,
    "email": "john@example.com",
    "service": "users-service"
})

# Interface Kibana: http://localhost:5601
# - Discover: Rechercher dans logs
# - Dashboard: Créer visualisations
# - Alerts: Configurer alertes

# ============================================================================
# DISTRIBUTED TRACING AVEC JAEGER
# ============================================================================

# PROBLÈME: Requête traverse 5 services, laquelle est lente? [REFLEXION]

# TRACE = Suivre une requête à travers TOUS les services

# Exemple:
# Client -> Gateway (10ms) -> Users (50ms) -> Orders (200ms) -> Payment (500ms)
# Total: 760ms
# 
# Jaeger montre:
# ┌─ Gateway ─────┐ 10ms
#   ┌─ Users ──────────┐ 50ms
#     ┌─ Orders ──────────────────────┐ 200ms
#       ┌─ Payment ─────────────────────────────────────────┐ 500ms
# 
# -> Payment est le goulot d'étranglement!

pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-fastapi
pip install opentelemetry-exporter-jaeger

# Configuration Jaeger
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

def setup_tracing(app, service_name="users-service"):
    """Configurer tracing pour FastAPI"""
    
    # Provider
    trace.set_tracer_provider(TracerProvider())
    tracer_provider = trace.get_tracer_provider()
    
    # Exporter vers Jaeger
    jaeger_exporter = JaegerExporter(
        agent_host_name="localhost",
        agent_port=6831,
    )
    
    tracer_provider.add_span_processor(
        BatchSpanProcessor(jaeger_exporter)
    )
    
    # Instrumenter FastAPI automatiquement
    FastAPIInstrumentor.instrument_app(app)
    
    return trace.get_tracer(service_name)

# Dans main.py
tracer = setup_tracing(app, "users-service")

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    # Créer span personnalisé
    with tracer.start_as_current_span("get_user_from_db"):
        user = await db.get(User, user_id)
    
    with tracer.start_as_current_span("serialize_user"):
        return {"id": user.id, "email": user.email}

# Lancer Jaeger (Docker)
docker run -d \
  --name jaeger \
  -p 6831:6831/udp \
  -p 16686:16686 \
  jaegertracing/all-in-one:latest

# Interface: http://localhost:16686
# - Rechercher traces par service
# - Voir timeline détaillée
# - Identifier goulots d'étranglement

# ============================================================================
# RÉSUMÉ MONITORING
# ============================================================================

# MÉTRIQUES (Prometheus + Grafana):
# [OK] Chiffres: CPU, RAM, requêtes/sec, latence
# [OK] Alertes si problème
# [OK] Dashboards temps réel

# LOGS (ELK Stack):
# [OK] Messages détaillés
# [OK] Structured logging (JSON)
# [OK] Correlation ID pour tracer requêtes
# [OK] Recherche puissante

# TRACES (Jaeger):
# [OK] Suivi requête à travers services
# [OK] Timeline visuelle
# [OK] Identifier goulots


[OK] TESTS - ASSURER LA QUALITÉ

# ============================================================================
# PYRAMIDE DES TESTS
# ============================================================================

# PYRAMIDE (du plus nombreux au moins nombreux):
#
#         /\
#        /E2\      <- E2E Tests (peu, lents, coûteux)
#       /────\
#      / Integ\    <- Integration Tests (moyennement)
#     /────────\
#    /   Unit   \  <- Unit Tests (beaucoup, rapides, pas chers)
#   /────────────\

# UNIT TESTS (70%):
# - Tester une fonction/classe isolée
# - Mocker dépendances externes
# - Très rapides (millisecondes)
# - Nombreux!

# INTEGRATION TESTS (20%):
# - Tester plusieurs composants ensemble
# - DB réelle, Redis réel
# - Plus lents (secondes)
# - Moins nombreux

# E2E TESTS (10%):
# - Tester tout le système
# - Tous les services actifs
# - Très lents (minutes)
# - Peu nombreux, seulement scénarios critiques

# ============================================================================
# TESTS UNITAIRES AVEC PYTEST
# ============================================================================

pip install pytest pytest-asyncio pytest-cov httpx

# Structure dossier tests/
# tests/
#   conftest.py        # Configuration pytest
#   test_auth.py       # Tests auth
#   test_users.py      # Tests users
#   test_orders.py     # Tests orders

# ========== CONFIGURATION PYTEST ==========

# tests/conftest.py
import pytest
from fastapi.testclient import TestClient
from httpx import AsyncClient
from main import app

@pytest.fixture
def client():
    """
    Fixture: Client de test synchrone
    
    Usage:
    def test_something(client):
        response = client.get("/users")
    """
    return TestClient(app)

@pytest.fixture
async def async_client():
    """
    Fixture: Client de test asynchrone
    
    Usage:
    async def test_something(async_client):
        response = await async_client.get("/users")
    """
    async with AsyncClient(app=app, base_url="http://test") as client:
        yield client

# ========== TESTS BASIQUES ==========

# tests/test_users.py
import pytest
from fastapi import status

def test_root_endpoint(client):
    """Test endpoint racine"""
    response = client.get("/")
    
    assert response.status_code == 200
    assert response.json() == {"service": "users", "status": "running"}

def test_health_check(client):
    """Test health check"""
    response = client.get("/health")
    
    assert response.status_code == 200
    assert response.json() == {"status": "healthy"}

@pytest.mark.asyncio
async def test_create_user(async_client):
    """
    Test création utilisateur
    
    @pytest.mark.asyncio = test asynchrone
    """
    # Données de test
    user_data = {
        "name": "John Doe",
        "email": "john@example.com",
        "password": "secret123"
    }
    
    # Faire requête
    response = await async_client.post("/users", json=user_data)
    
    # Vérifications (assertions)
    assert response.status_code == 201  # Created
    
    data = response.json()
    assert data["name"] == "John Doe"
    assert data["email"] == "john@example.com"
    assert "id" in data  # ID généré
    assert "password" not in data  # Password pas retourné!

@pytest.mark.asyncio
async def test_get_user(async_client):
    """Test récupération utilisateur"""
    
    # ARRANGE: Créer un user
    create_response = await async_client.post("/users", json={
        "name": "Jane Doe",
        "email": "jane@example.com",
        "password": "secret123"
    })
    user_id = create_response.json()["id"]
    
    # ACT: Récupérer ce user
    response = await async_client.get(f"/users/{user_id}")
    
    # ASSERT: Vérifier
    assert response.status_code == 200
    data = response.json()
    assert data["id"] == user_id
    assert data["name"] == "Jane Doe"

@pytest.mark.asyncio
async def test_get_nonexistent_user(async_client):
    """Test récupération user inexistant"""
    response = await async_client.get("/users/99999")
    
    assert response.status_code == 404
    assert response.json() == {"detail": "User not found"}

@pytest.mark.asyncio
async def test_update_user(async_client):
    """Test mise à jour utilisateur"""
    
    # Créer user
    create_response = await async_client.post("/users", json={
        "name": "Bob",
        "email": "bob@example.com",
        "password": "secret123"
    })
    user_id = create_response.json()["id"]
    
    # Mettre à jour
    response = await async_client.put(f"/users/{user_id}", json={
        "name": "Bob Updated",
        "email": "bob.new@example.com"
    })
    
    assert response.status_code == 200
    data = response.json()
    assert data["name"] == "Bob Updated"
    assert data["email"] == "bob.new@example.com"

@pytest.mark.asyncio
async def test_delete_user(async_client):
    """Test suppression utilisateur"""
    
    # Créer user
    create_response = await async_client.post("/users", json={
        "name": "Alice",
        "email": "alice@example.com",
        "password": "secret123"
    })
    user_id = create_response.json()["id"]
    
    # Supprimer
    delete_response = await async_client.delete(f"/users/{user_id}")
    assert delete_response.status_code == 204  # No Content
    
    # Vérifier suppression
    get_response = await async_client.get(f"/users/{user_id}")
    assert get_response.status_code == 404

# ========== TESTS AVEC MOCKING ==========

# MOCKING = Simuler dépendances externes

from unittest.mock import AsyncMock, patch, MagicMock

@pytest.mark.asyncio
@patch('services.email_service.send_email')
async def test_create_user_sends_email(mock_send_email, async_client):
    """
    Test que la création d'user envoie un email
    
    @patch = Remplacer send_email par un mock
    """
    
    # Configurer le mock
    mock_send_email.return_value = {"status": "sent"}
    
    # Créer user
    response = await async_client.post("/users", json={
        "name": "Test",
        "email": "test@example.com",
        "password": "secret123"
    })
    
    assert response.status_code == 201
    
    # Vérifier que send_email a été appelé
    mock_send_email.assert_called_once()
    
    # Vérifier les arguments
    args, kwargs = mock_send_email.call_args
    assert kwargs["to"] == "test@example.com"
    assert "Welcome" in kwargs["subject"]

@pytest.mark.asyncio
async def test_get_user_orders_with_mock(async_client, mocker):
    """
    Mocker appel à Orders Service
    
    mocker = fixture pytest-mock
    """
    
    # Mock appel HTTP externe
    mock_orders_client = mocker.patch('services.orders_client.get_user_orders')
    mock_orders_client.return_value = [
        {"id": 1, "total": 99.99},
        {"id": 2, "total": 149.99}
    ]
    
    # Appeler endpoint
    response = await async_client.get("/users/1/orders")
    
    assert response.status_code == 200
    data = response.json()
    assert len(data["orders"]) == 2
    assert data["orders"][0]["total"] == 99.99

# ========== FIXTURES RÉUTILISABLES ==========

# tests/conftest.py
@pytest.fixture
async def test_user(async_client):
    """
    Fixture: Créer un user de test
    
    Usage:
    async def test_something(test_user):
        # test_user est déjà créé!
        user_id = test_user["id"]
    """
    response = await async_client.post("/users", json={
        "name": "Test User",
        "email": "test@example.com",
        "password": "secret123"
    })
    return response.json()

@pytest.fixture
async def auth_token(async_client, test_user):
    """Fixture: Obtenir token JWT"""
    response = await async_client.post("/login", json={
        "email": test_user["email"],
        "password": "secret123"
    })
    return response.json()["access_token"]

# Utilisation:
@pytest.mark.asyncio
async def test_protected_endpoint(async_client, auth_token):
    """Test endpoint protégé"""
    headers = {"Authorization": f"Bearer {auth_token}"}
    
    response = await async_client.get("/me", headers=headers)
    
    assert response.status_code == 200

# ========== TESTS PARAMÉTRÉS ==========

@pytest.mark.parametrize("email,password,expected_status", [
    ("valid@example.com", "secret123", 201),  # Valid
    ("invalid-email", "secret123", 422),      # Email invalide
    ("valid@example.com", "short", 422),      # Password trop court
    ("", "secret123", 422),                   # Email vide
])
@pytest.mark.asyncio
async def test_user_validation(async_client, email, password, expected_status):
    """
    Test validation avec plusieurs cas
    
    @pytest.mark.parametrize = Exécuter test plusieurs fois avec données différentes
    """
    response = await async_client.post("/users", json={
        "name": "Test",
        "email": email,
        "password": password
    })
    
    assert response.status_code == expected_status

# ============================================================================
# TESTS D'INTÉGRATION
# ============================================================================

# Tests avec vraie DB, Redis, etc.

# tests/conftest.py
import asyncio
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from database import Base

# DB de test (séparée de prod!)
TEST_DATABASE_URL = "postgresql+asyncpg://test:test@localhost:5432/test_db"

@pytest.fixture(scope="session")
def event_loop():
    """Event loop pour toute la session de tests"""
    loop = asyncio.get_event_loop_policy().new_event_loop()
    yield loop
    loop.close()

@pytest.fixture(scope="session")
async def test_db():
    """
    DB de test
    
    scope="session" = Créée une fois pour tous les tests
    """
    # Créer engine
    engine = create_async_engine(TEST_DATABASE_URL)
    
    # Créer toutes les tables
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)
    
    yield engine
    
    # Cleanup: Supprimer tables après tests
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.drop_all)
    
    await engine.dispose()

@pytest.fixture
async def db_session(test_db):
    """
    Session DB pour un test
    
    Chaque test a sa propre session
    """
    async with AsyncSession(test_db) as session:
        yield session
        # Rollback après chaque test (DB propre)
        await session.rollback()

# Test d'intégration avec vraie DB
@pytest.mark.asyncio
async def test_create_user_in_db(db_session):
    """Test création user dans vraie DB"""
    from models import User
    from sqlalchemy import select
    
    # Créer user
    user = User(
        name="DB Test User",
        email="dbtest@example.com",
        password_hash="hashed"
    )
    db_session.add(user)
    await db_session.commit()
    await db_session.refresh(user)
    
    # Vérifier qu'il existe
    stmt = select(User).where(User.email == "dbtest@example.com")
    result = await db_session.execute(stmt)
    found_user = result.scalar_one()
    
    assert found_user.name == "DB Test User"
    assert found_user.id is not None

# ============================================================================
# COVERAGE (COUVERTURE DES TESTS)
# ============================================================================

# Coverage = % de code testé

# Lancer tests avec coverage:
pytest --cov=. --cov-report=html --cov-report=term

# Output:
# Name                Stmts   Miss  Cover
# ---------------------------------------
# main.py               120     10    92%
# models.py              45      5    89%
# services/auth.py       80     15    81%
# ---------------------------------------
# TOTAL                 245     30    88%

# Rapport HTML: htmlcov/index.html
# Montre lignes non testées en rouge!

# OBJECTIF: 80%+ coverage
# Mais 100% coverage ≠ 0 bugs!
# Qualité des tests > quantité

# ============================================================================
# TESTS DE CHARGE (LOAD TESTING)
# ============================================================================

pip install locust

# POURQUOI?
# Vérifier que votre service tient la charge
# Combien de requêtes/seconde? Latence sous charge?

# locustfile.py
from locust import HttpUser, task, between
import random

class MicroserviceUser(HttpUser):
    """
    Simuler comportement utilisateur
    
    Locust va créer des milliers d'instances
    de cette classe pour générer du trafic!
    """
    
    wait_time = between(1, 3)  # Attendre 1-3s entre requêtes
    
    def on_start(self):
        """Exécuté au démarrage de chaque utilisateur"""
        # Login une fois au début
        response = self.client.post("/login", json={
            "email": "test@example.com",
            "password": "secret123"
        })
        
        if response.status_code == 200:
            self.token = response.json()["access_token"]
        else:
            self.token = None
    
    @task(3)  # Poids 3 (3x plus exécuté que @task(1))
    def get_users(self):
        """Récupérer liste utilisateurs"""
        headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}
        
        with self.client.get("/users", headers=headers, catch_response=True) as response:
            if response.status_code == 200:
                response.success()
            else:
                response.failure(f"Failed with status {response.status_code}")
    
    @task(2)
    def get_user(self):
        """Récupérer utilisateur spécifique"""
        user_id = random.randint(1, 100)
        headers = {"Authorization": f"Bearer {self.token}"} if self.token else {}
        
        self.client.get(f"/users/{user_id}", headers=headers)
    
    @task(1)
    def create_user(self):
        """Créer utilisateur"""
        user_number = random.randint(1000, 9999)
        
        self.client.post("/users", json={
            "name": f"User {user_number}",
            "email": f"user{user_number}@example.com",
            "password": "secret123"
        })
    
    @task(1)
    def search_users(self):
        """Rechercher utilisateurs"""
        search_term = random.choice(["john", "jane", "bob", "alice"])
        
        self.client.get(f"/users/search?name={search_term}")

# Lancer Locust:
locust -f locustfile.py --host=http://localhost:8001

# Interface web: http://localhost:8089
# 
# Configurer:
# - Number of users: 100 (100 utilisateurs simultanés)
# - Spawn rate: 10 (ajouter 10 users/sec)
# 
# Démarrer et observer:
# - Requêtes/sec (RPS)
# - Temps de réponse (P50, P95, P99)
# - Taux d'erreurs
# - Nombre d'utilisateurs actifs

# SCÉNARIOS TYPIQUES:

# Test 1: Charge normale
# - 100 users
# - 10 RPS spawn rate
# - Durée: 5 minutes
# Objectif: Vérifier comportement normal

# Test 2: Pic de charge (Black Friday)
# - 1000 users
# - 50 RPS spawn rate
# - Durée: 10 minutes
# Objectif: Tenir le pic

# Test 3: Soak test (endurance)
# - 500 users
# - Durée: 2 heures
# Objectif: Pas de memory leak, performance stable

# Test 4: Spike test
# - 0 -> 1000 users en 10 secondes
# Objectif: Réaction au spike soudain

# ============================================================================
# TESTS CONTRACT (PACT)
# ============================================================================

# PROBLÈME: Orders Service appelle Users Service
# Si Users change son API -> Orders casse!

# SOLUTION: Contract Testing
# Définir un "contrat" que Users doit respecter

pip install pact-python

# Consumer (Orders Service): Définit contrat
# tests/test_users_contract.py

from pact import Consumer, Provider

pact = Consumer('OrdersService').has_pact_with(Provider('UsersService'))

def test_get_user_contract():
    """
    Contrat: GET /users/{id}
    
    Orders Service attend:
    - Status 200
    - JSON avec id, email, name
    """
    
    expected_response = {
        'id': 123,
        'email': 'john@example.com',
        'name': 'John Doe',
        'active': True
    }
    
    (pact
     .given('user 123 exists')
     .upon_receiving('a request for user 123')
     .with_request('GET', '/users/123')
     .will_respond_with(200, body=expected_response))
    
    with pact:
        # Faire vraie requête (pact mock le serveur)
        response = requests.get(f'{pact.uri}/users/123')
        
        assert response.json() == expected_response

# Génère fichier: pacts/OrdersService-UsersService.json

# Provider (Users Service): Vérifie contrat
# tests/test_provider_contract.py

from pact import Verifier

def test_users_service_honors_contract():
    """
    Vérifier que Users Service respecte le contrat
    """
    
    verifier = Verifier(provider='UsersService', provider_base_url='http://localhost:8001')
    
    # Vérifier contre le contrat
    output, logs = verifier.verify_pacts(
        './pacts/OrdersService-UsersService.json',
        provider_states_setup_url='http://localhost:8001/setup'
    )
    
    assert output == 0  # Succès

# AVANTAGE:
# Si Users change API -> Tests provider échouent
# Évite casser Orders en production! [BRAVO]

# ============================================================================
# RÉSUMÉ TESTS
# ============================================================================

# TYPES DE TESTS:
# 1. Unit Tests (70%): Fonctions isolées, mocks
# 2. Integration Tests (20%): Composants ensemble, vraie DB
# 3. E2E Tests (10%): Système complet
# 4. Load Tests: Performance sous charge
# 5. Contract Tests: Compatibilité entre services

# OUTILS:
# - pytest: Framework de tests
# - pytest-asyncio: Tests async
# - pytest-cov: Coverage
# - unittest.mock: Mocking
# - locust: Load testing
# - pact: Contract testing

# BONNES PRATIQUES:
# [OK] Tests automatisés dans CI/CD
# [OK] Coverage > 80%
# [OK] Tests rapides (<5min pour tout)
# [OK] Tests isolés (pas de dépendances entre tests)
# [OK] Noms descriptifs (test_create_user_sends_welcome_email)
# [OK] Arrange-Act-Assert pattern
# [OK] Fixtures pour réutilisation
# [OK] DB de test séparée


[OK] API GATEWAY - POINT D'ENTRÉE UNIQUE

# ============================================================================
# QU'EST-CE QU'UN API GATEWAY ?
# ============================================================================

# PROBLÈME SANS GATEWAY:
# Client doit connaître TOUS les services:
# - users-service.example.com:8001
# - orders-service.example.com:8002
# - products-service.example.com:8003
# - payments-service.example.com:8004
# 
# [X] Client couplé à l'architecture interne
# [X] Gestion auth dans chaque service
# [X] CORS config partout
# [X] Rate limiting dupliqué

# SOLUTION: API GATEWAY
# Client -> Gateway (1 seule URL) -> Services backend
# 
# [OK] Point d'entrée unique: api.example.com
# [OK] Routage intelligent
# [OK] Auth centralisée
# [OK] Rate limiting centralisé
# [OK] Transformation requêtes/réponses
# [OK] Cache
# [OK] Load balancing

# ANALOGIE:
# Sans Gateway = Appeler directement chaque employé d'une entreprise
# Avec Gateway = Réceptionniste qui route les appels

# ============================================================================
# FONCTIONNALITÉS CLÉS D'UN API GATEWAY
# ============================================================================

# 1. ROUTING (Routage)
#    /users/* -> users-service
#    /orders/* -> orders-service
#    /products/* -> products-service

# 2. AUTHENTICATION & AUTHORIZATION
#    Vérifier JWT une fois, propager user info

# 3. RATE LIMITING
#    Max 100 req/min par IP
#    Max 1000 req/hour par user

# 4. CIRCUIT BREAKER
#    Si service down -> réponse rapide, pas timeout

# 5. REQUEST/RESPONSE TRANSFORMATION
#    Ajouter headers, modifier format

# 6. AGGREGATION
#    1 requête client -> plusieurs requêtes backend
#    Combiner réponses

# 7. CACHING
#    Mettre en cache réponses fréquentes

# 8. LOAD BALANCING
#    Distribuer requêtes entre instances

# 9. LOGGING & MONITORING
#    Logs centralisés, métriques

# ============================================================================
# API GATEWAY AVEC FASTAPI (CUSTOM)
# ============================================================================

from fastapi import FastAPI, Request, HTTPException, Depends
from fastapi.responses import Response, JSONResponse
import httpx
from typing import Optional
import time
from collections import defaultdict
from datetime import datetime, timedelta
import asyncio

app = FastAPI(title="API Gateway")

# ============================================================================
# CONFIGURATION DES SERVICES BACKEND
# ============================================================================

# Mapping routes -> services
SERVICE_ROUTES = {
    "/users": {
        "url": "http://users-service:8001",
        "timeout": 5.0,
        "retry": 3
    },
    "/orders": {
        "url": "http://orders-service:8002",
        "timeout": 10.0,  # Plus long pour orders
        "retry": 2
    },
    "/products": {
        "url": "http://products-service:8003",
        "timeout": 5.0,
        "retry": 3
    },
    "/payments": {
        "url": "http://payments-service:8004",
        "timeout": 15.0,  # Encore plus long pour payments
        "retry": 1
    }
}

def get_service_config(path: str):
    """
    Trouver config du service basé sur le path
    
    /users/123 -> config users-service
    """
    for prefix, config in SERVICE_ROUTES.items():
        if path.startswith(prefix):
            return prefix, config
    return None, None

# ============================================================================
# RATE LIMITING
# ============================================================================

class RateLimiter:
    """
    Rate limiter en mémoire
    
    Production: Utiliser Redis pour partager entre instances!
    """
    
    def __init__(self):
        # {key: [(timestamp, count), ...]}
        self.requests = defaultdict(list)
        self.lock = asyncio.Lock()
    
    async def is_allowed(
        self,
        key: str,
        max_requests: int = 100,
        window_seconds: int = 60
    ) -> tuple[bool, Optional[int]]:
        """
        Vérifier si requête autorisée
        
        Returns:
            (allowed, retry_after_seconds)
        """
        async with self.lock:
            now = datetime.utcnow()
            window_start = now - timedelta(seconds=window_seconds)
            
            # Nettoyer vieilles requêtes
            self.requests[key] = [
                (ts, count) for ts, count in self.requests[key]
                if ts > window_start
            ]
            
            # Compter requêtes dans la fenêtre
            total = sum(count for _, count in self.requests[key])
            
            if total >= max_requests:
                # Limite atteinte
                oldest = self.requests[key][0][0] if self.requests[key] else now
                retry_after = int((oldest + timedelta(seconds=window_seconds) - now).total_seconds())
                return False, retry_after
            
            # Ajouter requête actuelle
            self.requests[key].append((now, 1))
            return True, None

rate_limiter = RateLimiter()

@app.middleware("http")
async def rate_limit_middleware(request: Request, call_next):
    """
    Middleware: Rate limiting par IP
    """
    
    # Exclure health check
    if request.url.path == "/health":
        return await call_next(request)
    
    # Clé: IP du client
    client_ip = request.client.host
    
    # Vérifier rate limit
    allowed, retry_after = await rate_limiter.is_allowed(
        key=f"ip:{client_ip}",
        max_requests=100,  # 100 requêtes
        window_seconds=60   # par minute
    )
    
    if not allowed:
        return JSONResponse(
            status_code=429,  # Too Many Requests
            content={
                "error": "Rate limit exceeded",
                "retry_after": retry_after
            },
            headers={"Retry-After": str(retry_after)}
        )
    
    return await call_next(request)

# ============================================================================
# CIRCUIT BREAKER
# ============================================================================

class CircuitBreaker:
    """
    Circuit Breaker pattern
    
    États:
    - CLOSED: Normal, requêtes passent
    - OPEN: Service down, requêtes bloquées
    - HALF_OPEN: Test si service revenu
    """
    
    def __init__(
        self,
        failure_threshold: int = 5,     # Ouvrir après 5 échecs
        timeout: int = 60,              # Rester ouvert 60s
        success_threshold: int = 2      # Fermer après 2 succès
    ):
        self.failure_threshold = failure_threshold
        self.timeout = timeout
        self.success_threshold = success_threshold
        
        self.failures = 0
        self.successes = 0
        self.last_failure_time = None
        self.state = "CLOSED"  # CLOSED, OPEN, HALF_OPEN
    
    async def call(self, func, *args, **kwargs):
        """
        Exécuter fonction avec circuit breaker
        """
        
        # Si OPEN, vérifier timeout
        if self.state == "OPEN":
            if datetime.utcnow() - self.last_failure_time > timedelta(seconds=self.timeout):
                # Passer en HALF_OPEN (tester)
                self.state = "HALF_OPEN"
                self.successes = 0
            else:
                # Toujours OPEN, rejeter
                raise HTTPException(
                    status_code=503,
                    detail="Service temporarily unavailable (circuit breaker open)"
                )
        
        try:
            # Exécuter fonction
            result = await func(*args, **kwargs)
            
            # Succès!
            self.on_success()
            return result
        
        except Exception as e:
            # Échec
            self.on_failure()
            raise e
    
    def on_success(self):
        """Enregistrer succès"""
        self.failures = 0
        
        if self.state == "HALF_OPEN":
            self.successes += 1
            if self.successes >= self.success_threshold:
                # Assez de succès, fermer circuit
                self.state = "CLOSED"
    
    def on_failure(self):
        """Enregistrer échec"""
        self.failures += 1
        self.last_failure_time = datetime.utcnow()
        
        if self.failures >= self.failure_threshold:
            # Trop d'échecs, ouvrir circuit
            self.state = "OPEN"

# Circuit breakers par service
circuit_breakers = {
    service: CircuitBreaker()
    for service in SERVICE_ROUTES.keys()
}

# ============================================================================
# ROUTAGE & FORWARDING
# ============================================================================

async def forward_request(
    service_url: str,
    path: str,
    request: Request,
    timeout: float = 5.0,
    retry: int = 3
) -> Response:
    """
    Transférer requête au service backend
    
    Gère:
    - Retry automatique
    - Timeout
    - Headers forwarding
    """
    
    # Construire URL complète
    url = f"{service_url}{path}"
    
    # Copier headers (sauf Host)
    headers = dict(request.headers)
    headers.pop('host', None)
    
    # Ajouter headers custom
    headers['X-Forwarded-For'] = request.client.host
    headers['X-Gateway-Timestamp'] = str(time.time())
    
    # Lire body une fois
    body = await request.body()
    
    # Retry loop
    last_exception = None
    for attempt in range(retry):
        try:
            async with httpx.AsyncClient(timeout=timeout) as client:
                
                # Forward requête
                response = await client.request(
                    method=request.method,
                    url=url,
                    headers=headers,
                    content=body,
                    params=request.query_params
                )
                
                # Retourner réponse
                return Response(
                    content=response.content,
                    status_code=response.status_code,
                    headers=dict(response.headers)
                )
        
        except httpx.TimeoutException as e:
            last_exception = e
            if attempt < retry - 1:
                # Attendre avant retry (exponential backoff)
                await asyncio.sleep(2 ** attempt)
        
        except httpx.ConnectError as e:
            last_exception = e
            if attempt < retry - 1:
                await asyncio.sleep(2 ** attempt)
    
    # Tous les retries ont échoué
    raise HTTPException(
        status_code=503,
        detail=f"Service unavailable: {str(last_exception)}"
    )

@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def gateway_route(path: str, request: Request):
    """
    Route principale: Forward toutes les requêtes
    """
    
    # Trouver service
    prefix, config = get_service_config(f"/{path}")
    
    if not config:
        raise HTTPException(
            status_code=404,
            detail="Service not found"
        )
    
    # Appliquer circuit breaker
    breaker = circuit_breakers.get(prefix)
    
    if breaker:
        return await breaker.call(
            forward_request,
            service_url=config["url"],
            path=f"/{path}",
            request=request,
            timeout=config["timeout"],
            retry=config["retry"]
        )
    else:
        return await forward_request(
            service_url=config["url"],
            path=f"/{path}",
            request=request,
            timeout=config["timeout"],
            retry=config["retry"]
        )

# ============================================================================
# AUTHENTIFICATION CENTRALISÉE
# ============================================================================

from auth import decode_token  # Réutiliser fonction JWT

@app.middleware("http")
async def auth_middleware(request: Request, call_next):
    """
    Middleware: Vérifier JWT pour routes protégées
    """
    
    # Routes publiques (pas d'auth)
    public_paths = ["/health", "/login", "/register", "/docs", "/openapi.json"]
    if any(request.url.path.startswith(p) for p in public_paths):
        return await call_next(request)
    
    # Extraire token
    auth_header = request.headers.get("Authorization")
    if not auth_header or not auth_header.startswith("Bearer "):
        return JSONResponse(
            status_code=401,
            content={"error": "Missing or invalid Authorization header"}
        )
    
    token = auth_header.split(" ")[1]
    
    # Vérifier token
    try:
        payload = decode_token(token)
        
        # Ajouter user info au state (accessible dans routes)
        request.state.user = payload
        
        # Propager user info au service backend (header X-User-*)
        # On va modifier les headers de la requête forward
        
    except HTTPException as e:
        return JSONResponse(
            status_code=401,
            content={"error": "Invalid token"}
        )
    
    response = await call_next(request)
    return response

# Modifier forward_request pour ajouter user headers:
async def forward_request_with_user(
    service_url: str,
    path: str,
    request: Request,
    timeout: float = 5.0,
    retry: int = 3
) -> Response:
    """Version avec propagation user info"""
    
    url = f"{service_url}{path}"
    headers = dict(request.headers)
    headers.pop('host', None)
    
    # Ajouter user info si disponible
    if hasattr(request.state, 'user'):
        user = request.state.user
        headers['X-User-ID'] = str(user.get('sub'))
        headers['X-User-Email'] = str(user.get('email', ''))
        headers['X-User-Role'] = str(user.get('role', ''))
    
    headers['X-Forwarded-For'] = request.client.host
    
    body = await request.body()
    
    # ... reste identique ...
    
    async with httpx.AsyncClient(timeout=timeout) as client:
        response = await client.request(
            method=request.method,
            url=url,
            headers=headers,
            content=body,
            params=request.query_params
        )
        
        return Response(
            content=response.content,
            status_code=response.status_code,
            headers=dict(response.headers)
        )

# ============================================================================
# AGGREGATION (Combiner plusieurs services)
# ============================================================================

@app.get("/api/user-dashboard/{user_id}")
async def get_user_dashboard(user_id: int, request: Request):
    """
    Agréger données de plusieurs services
    
    1 requête client -> 3 requêtes backend (parallel!)
    - User info (Users Service)
    - Orders (Orders Service)
    - Recommendations (Products Service)
    """
    
    # Vérifier auth
    if not hasattr(request.state, 'user'):
        raise HTTPException(status_code=401)
    
    async with httpx.AsyncClient(timeout=10.0) as client:
        
        # Lancer 3 requêtes en parallèle!
        user_task = client.get(f"http://users-service:8001/users/{user_id}")
        orders_task = client.get(f"http://orders-service:8002/users/{user_id}/orders")
        reco_task = client.get(f"http://products-service:8003/recommendations/{user_id}")
        
        # Attendre toutes les réponses
        responses = await asyncio.gather(
            user_task,
            orders_task,
            reco_task,
            return_exceptions=True  # Ne pas fail si 1 service down
        )
        
        # Extraire données
        user_data = responses[0].json() if not isinstance(responses[0], Exception) else None
        orders_data = responses[1].json() if not isinstance(responses[1], Exception) else []
        reco_data = responses[2].json() if not isinstance(responses[2], Exception) else []
        
        # Combiner
        return {
            "user": user_data,
            "recent_orders": orders_data[:5],  # 5 dernières commandes
            "recommendations": reco_data[:10],  # 10 recommandations
            "stats": {
                "total_orders": len(orders_data),
                "total_spent": sum(o.get("total", 0) for o in orders_data)
            }
        }

# ============================================================================
# CACHING
# ============================================================================

from functools import wraps
import hashlib
import json

# Cache en mémoire (production: Redis!)
cache_store = {}

def cache_response(ttl: int = 300):
    """
    Décorateur: Mettre en cache réponse
    
    Usage:
    @cache_response(ttl=60)
    @app.get("/products")
    async def list_products():
        ...
    """
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            # Générer clé cache
            cache_key = f"{func.__name__}:{hashlib.md5(str(args).encode() + str(kwargs).encode()).hexdigest()}"
            
            # Vérifier cache
            if cache_key in cache_store:
                cached_data, cached_time = cache_store[cache_key]
                
                # Vérifier expiration
                if time.time() - cached_time < ttl:
                    return cached_data
            
            # Exécuter fonction
            result = await func(*args, **kwargs)
            
            # Mettre en cache
            cache_store[cache_key] = (result, time.time())
            
            return result
        
        return wrapper
    return decorator

@app.get("/api/products/popular")
@cache_response(ttl=300)  # Cache 5 minutes
async def get_popular_products():
    """Liste produits populaires (mise à jour peu fréquente)"""
    async with httpx.AsyncClient() as client:
        response = await client.get("http://products-service:8003/products/popular")
        return response.json()

# ============================================================================
# TRANSFORMATION REQUÊTE/RÉPONSE
# ============================================================================

@app.middleware("http")
async def transform_middleware(request: Request, call_next):
    """
    Transformer requêtes/réponses
    """
    
    # Ajouter headers communs
    # (déjà fait dans forward_request)
    
    # Exécuter requête
    response = await call_next(request)
    
    # Ajouter headers à la réponse
    response.headers["X-Gateway-Version"] = "1.0.0"
    response.headers["X-Response-Time"] = str(time.time())
    
    # Transformation du body (si nécessaire)
    # Exemple: Wrapper toutes les réponses
    # {"data": {...}, "meta": {"timestamp": ...}}
    
    return response

# ============================================================================
# LOAD BALANCING
# ============================================================================

import random

# Multiple instances par service
SERVICE_INSTANCES = {
    "/users": [
        "http://users-service-1:8001",
        "http://users-service-2:8001",
        "http://users-service-3:8001"
    ],
    "/orders": [
        "http://orders-service-1:8002",
        "http://orders-service-2:8002"
    ]
}

def get_service_instance(prefix: str) -> str:
    """
    Choisir instance avec load balancing
    
    Stratégies:
    - Round-robin
    - Random
    - Least connections
    - Weighted
    """
    
    instances = SERVICE_INSTANCES.get(prefix, [])
    
    if not instances:
        return SERVICE_ROUTES[prefix]["url"]
    
    # Random selection (simple)
    return random.choice(instances)

# Modifier gateway_route pour utiliser load balancing:
@app.api_route("/{path:path}", methods=["GET", "POST", "PUT", "DELETE", "PATCH"])
async def gateway_route_with_lb(path: str, request: Request):
    """Route avec load balancing"""
    
    prefix, config = get_service_config(f"/{path}")
    
    if not config:
        raise HTTPException(status_code=404, detail="Service not found")
    
    # Choisir instance (load balancing)
    service_url = get_service_instance(prefix)
    
    breaker = circuit_breakers.get(prefix)
    
    if breaker:
        return await breaker.call(
            forward_request_with_user,
            service_url=service_url,
            path=f"/{path}",
            request=request,
            timeout=config["timeout"],
            retry=config["retry"]
        )
    else:
        return await forward_request_with_user(
            service_url=service_url,
            path=f"/{path}",
            request=request,
            timeout=config["timeout"],
            retry=config["retry"]
        )

# ============================================================================
# MONITORING GATEWAY
# ============================================================================

from prometheus_client import Counter, Histogram

# Métriques gateway
gateway_requests = Counter(
    'gateway_requests_total',
    'Total requests through gateway',
    ['method', 'service', 'status']
)

gateway_duration = Histogram(
    'gateway_request_duration_seconds',
    'Request duration through gateway',
    ['method', 'service']
)

@app.middleware("http")
async def metrics_middleware(request: Request, call_next):
    """Collecter métriques"""
    
    start_time = time.time()
    
    # Déterminer service
    prefix, _ = get_service_config(request.url.path)
    service = prefix.lstrip('/') if prefix else 'unknown'
    
    # Exécuter requête
    response = await call_next(request)
    
    # Enregistrer métriques
    duration = time.time() - start_time
    
    gateway_requests.labels(
        method=request.method,
        service=service,
        status=response.status_code
    ).inc()
    
    gateway_duration.labels(
        method=request.method,
        service=service
    ).observe(duration)
    
    return response

@app.get("/metrics")
async def metrics():
    """Endpoint métriques pour Prometheus"""
    from prometheus_client import generate_latest
    return Response(content=generate_latest(), media_type="text/plain")

# ============================================================================
# HEALTH CHECK
# ============================================================================

@app.get("/health")
async def health_check():
    """Health check simple"""
    return {"status": "healthy", "timestamp": time.time()}

@app.get("/health/ready")
async def readiness_check():
    """
    Readiness check: Vérifier tous les services backend
    """
    
    results = {}
    overall_healthy = True
    
    async with httpx.AsyncClient(timeout=3.0) as client:
        for prefix, config in SERVICE_ROUTES.items():
            service_name = prefix.lstrip('/')
            
            try:
                response = await client.get(f"{config['url']}/health")
                
                if response.status_code == 200:
                    results[service_name] = {"status": "healthy"}
                else:
                    results[service_name] = {"status": "unhealthy"}
                    overall_healthy = False
            
            except Exception as e:
                results[service_name] = {"status": "unhealthy", "error": str(e)}
                overall_healthy = False
    
    status_code = 200 if overall_healthy else 503
    
    return JSONResponse(
        status_code=status_code,
        content={
            "status": "healthy" if overall_healthy else "unhealthy",
            "services": results
        }
    )

# ============================================================================
# RÉSUMÉ API GATEWAY
# ============================================================================

# FONCTIONNALITÉS IMPLÉMENTÉES:
# [OK] Routing intelligent
# [OK] Rate limiting (par IP)
# [OK] Circuit breaker (protection services down)
# [OK] Retry automatique
# [OK] Timeout configurables
# [OK] Authentification centralisée (JWT)
# [OK] Propagation user info (X-User-* headers)
# [OK] Aggregation (combiner plusieurs services)
# [OK] Caching
# [OK] Load balancing (round-robin/random)
# [OK] Request/Response transformation
# [OK] Monitoring (métriques Prometheus)
# [OK] Health checks

# SOLUTIONS ENTERPRISE (Alternatives):
# - Kong: Gateway open-source très populaire
# - Traefik: Moderne, auto-découverte services
# - AWS API Gateway: Solution cloud AWS
# - Google Cloud API Gateway: Solution cloud GCP
# - Azure API Management: Solution cloud Azure
# - Ambassador: Kubernetes-native
# - Tyk: Open-source, dashboard UI

# EXEMPLE COMPLET D'UTILISATION:
"""
# Client fait 1 requête:
GET https://api.example.com/users/123
Authorization: Bearer <jwt>

# Gateway:
1. Rate limit check (OK)
2. Auth check (JWT valide)
3. Circuit breaker check (CLOSED)
4. Route vers users-service (load balanced)
5. Retry si échec
6. Transform response
7. Cache si applicable
8. Retourner au client

# Backend service reçoit:
GET http://users-service-2:8001/users/123
X-User-ID: 456
X-User-Email: john@example.com
X-User-Role: user
X-Forwarded-For: 192.168.1.100
X-Gateway-Timestamp: 1234567890.123

# Service backend peut:
- Utiliser X-User-* directement (pas besoin vérifier JWT!)
- Logger X-Forwarded-For
- Pas besoin auth middleware
"""

# BONNES PRATIQUES:
# [OK] Rate limiting avec Redis (pas en mémoire)
# [OK] Circuit breaker par service
# [OK] Retry avec exponential backoff
# [OK] Timeout adapté par service
# [OK] Caching pour données fréquentes
# [OK] Monitoring complet (Prometheus)
# [OK] Health checks détaillés
# [OK] CORS configuré proprement
# [OK] TLS/HTTPS obligatoire
# [OK] Logs structurés avec correlation ID


# ============================================================================
# [BRAVO] FIN DU CHEATSHEET MICROSERVICES PYTHON
# ============================================================================

# VOUS AVEZ MAINTENANT TOUTES LES CONNAISSANCES POUR:
# [OK] Créer microservices avec FastAPI
# [OK] Communiquer entre services (REST, gRPC, Message Queues)
# [OK] Containeriser avec Docker
# [OK] Orchestrer avec Kubernetes
# [OK] Sécuriser avec JWT
# [OK] Gérer bases de données (PostgreSQL, MongoDB)
# [OK] Monitor avec Prometheus/Grafana/Jaeger
# [OK] Tester (unitaires, intégration, charge)
# [OK] Créer API Gateway complet

# PROCHAINES ÉTAPES:
# 1. Commencer PETIT: 2-3 services maximum
# 2. Maîtriser Docker Compose
# 3. Ajouter monitoring dès le début
# 4. Tests automatisés dans CI/CD
# 5. Documentation (Swagger/OpenAPI)
# 6. Migrer progressivement vers Kubernetes

# RESSOURCES RECOMMANDÉES:
# - Documentation FastAPI: https://fastapi.tiangolo.com
# - Microservices Patterns: https://microservices.io
# - Docker Docs: https://docs.docker.com
# - Kubernetes Docs: https://kubernetes.io/docs
# - Martin Fowler Blog: https://martinfowler.com/microservices

# BON COURAGE! [RAPIDE])


# === gRPC (HAUTE PERFORMANCE) ===
pip install grpcio grpcio-tools

# user.proto
syntax = "proto3";

package users;

service UserService {
    rpc GetUser (GetUserRequest) returns (User);
    rpc CreateUser (CreateUserRequest) returns (User);
    rpc ListUsers (ListUsersRequest) returns (UserList);
}

message User {
    int32 id = 1;
    string name = 2;
    string email = 3;
    bool active = 4;
}

message GetUserRequest {
    int32 id = 1;
}

message CreateUserRequest {
    string name = 1;
    string email = 2;
}

message ListUsersRequest {}

message UserList {
    repeated User users = 1;
}

# Générer code Python
python -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. user.proto

# server.py
import grpc
from concurrent import futures
import user_pb2
import user_pb2_grpc

class UserServiceServicer(user_pb2_grpc.UserServiceServicer):
    def __init__(self):
        self.users_db = {}
    
    def GetUser(self, request, context):
        user_id = request.id
        if user_id not in self.users_db:
            context.set_code(grpc.StatusCode.NOT_FOUND)
            context.set_details("User not found")
            return user_pb2.User()
        return self.users_db[user_id]
    
    def CreateUser(self, request, context):
        user_id = len(self.users_db) + 1
        user = user_pb2.User(
            id=user_id,
            name=request.name,
            email=request.email,
            active=True
        )
        self.users_db[user_id] = user
        return user
    
    def ListUsers(self, request, context):
        users = list(self.users_db.values())
        return user_pb2.UserList(users=users)

def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    user_pb2_grpc.add_UserServiceServicer_to_server(
        UserServiceServicer(), server
    )
    server.add_insecure_port('[::]:50051')
    server.start()
    print("gRPC server started on port 50051")
    server.wait_for_termination()

if __name__ == '__main__':
    serve()

# client.py
import grpc
import user_pb2
import user_pb2_grpc

def run():
    with grpc.insecure_channel('localhost:50051') as channel:
        stub = user_pb2_grpc.UserServiceStub(channel)
        
        # Créer utilisateur
        user = stub.CreateUser(user_pb2.CreateUserRequest(
            name="John Doe",
            email="john@example.com"
        ))
        print(f"Created user: {user}")
        
        # Récupérer utilisateur
        user = stub.GetUser(user_pb2.GetUserRequest(id=user.id))
        print(f"Retrieved user: {user}")

if __name__ == '__main__':
    run()


# === MESSAGE QUEUES (ASYNCHRONE) ===

# RabbitMQ avec Pika
pip install pika

# publisher.py
import pika
import json

class MessagePublisher:
    def __init__(self, host='localhost'):
        self.connection = pika.BlockingConnection(
            pika.ConnectionParameters(host=host)
        )
        self.channel = self.connection.channel()
    
    def publish_event(self, exchange, routing_key, event):
        self.channel.exchange_declare(
            exchange=exchange,
            exchange_type='topic',
            durable=True
        )
        
        message = json.dumps(event)
        self.channel.basic_publish(
            exchange=exchange,
            routing_key=routing_key,
            body=message,
            properties=pika.BasicProperties(
                delivery_mode=2,  # persistent
                content_type='application/json'
            )
        )
        print(f"Published: {routing_key} -> {message}")
    
    def close(self):
        self.connection.close()

# Utilisation
publisher = MessagePublisher()
publisher.publish_event(
    exchange='users',
    routing_key='user.created',
    event={'user_id': 1, 'name': 'John Doe', 'email': 'john@example.com'}
)
publisher.close()

# consumer.py
import pika
import json

class MessageConsumer:
    def __init__(self, host='localhost'):
        self.connection = pika.BlockingConnection(
            pika.ConnectionParameters(host=host)
        )
        self.channel = self.connection.channel()
    
    def consume(self, exchange, queue, routing_keys, callback):
        self.channel.exchange_declare(
            exchange=exchange,
            exchange_type='topic',
            durable=True
        )
        
        self.channel.queue_declare(queue=queue, durable=True)
        
        for routing_key in routing_keys:
            self.channel.queue_bind(
                exchange=exchange,
                queue=queue,
                routing_key=routing_key
            )
        
        def on_message(ch, method, properties, body):
            event = json.loads(body)
            callback(event)
            ch.basic_ack(delivery_tag=method.delivery_tag)
        
        self.channel.basic_qos(prefetch_count=1)
        self.channel.basic_consume(
            queue=queue,
            on_message_callback=on_message
        )
        
        print(f"Consuming from queue: {queue}")
        self.channel.start_consuming()

# Utilisation
def handle_user_event(event):
    print(f"Received event: {event}")
    # Traiter l'événement

consumer = MessageConsumer()
consumer.consume(
    exchange='users',
    queue='notifications',
    routing_keys=['user.created', 'user.updated'],
    callback=handle_user_event
)


# Celery pour tâches asynchrones
pip install celery redis

# tasks.py
from celery import Celery

app = Celery('tasks', broker='redis://localhost:6379/0')

@app.task
def send_email(user_id, email, subject):
    # Simuler envoi email
    print(f"Sending email to {email}: {subject}")
    return f"Email sent to user {user_id}"

@app.task
def process_order(order_id):
    print(f"Processing order {order_id}")
    return f"Order {order_id} processed"

# Lancer worker
# celery -A tasks worker --loglevel=info

# Utiliser depuis service
from tasks import send_email, process_order

# Synchrone
result = send_email.delay(1, 'user@example.com', 'Welcome!')

# Asynchrone avec callback
result = process_order.apply_async(args=[123], countdown=10)


# Kafka avec confluent-kafka
pip install confluent-kafka

# producer.py
from confluent_kafka import Producer
import json

def delivery_report(err, msg):
    if err:
        print(f'Delivery failed: {err}')
    else:
        print(f'Message delivered to {msg.topic()} [{msg.partition()}]')

producer = Producer({'bootstrap.servers': 'localhost:9092'})

event = {
    'user_id': 1,
    'name': 'John Doe',
    'email': 'john@example.com'
}

producer.produce(
    'user-events',
    key='user.created',
    value=json.dumps(event),
    callback=delivery_report
)
producer.flush()

# consumer.py
from confluent_kafka import Consumer, KafkaError
import json

consumer = Consumer({
    'bootstrap.servers': 'localhost:9092',
    'group.id': 'notification-service',
    'auto.offset.reset': 'earliest'
})

consumer.subscribe(['user-events'])

try:
    while True:
        msg = consumer.poll(1.0)
        
        if msg is None:
            continue
        if msg.error():
            if msg.error().code() == KafkaError._PARTITION_EOF:
                continue
            else:
                print(f'Error: {msg.error()}')
                break
        
        event = json.loads(msg.value().decode('utf-8'))
        print(f'Received: {event}')
        # Traiter l'événement

except KeyboardInterrupt:
    pass
finally:
    consumer.close()


[OK] SERVICE DISCOVERY & LOAD BALANCING

# === CONSUL ===
pip install python-consul

# service_registry.py
import consul
import socket

class ServiceRegistry:
    def __init__(self, consul_host='localhost', consul_port=8500):
        self.consul = consul.Consul(host=consul_host, port=consul_port)
    
    def register_service(self, service_name, service_id, host, port):
        """Enregistrer un service"""
        self.consul.agent.service.register(
            name=service_name,
            service_id=service_id,
            address=host,
            port=port,
            check=consul.Check.http(
                f'http://{host}:{port}/health',
                interval='10s',
                timeout='5s'
            )
        )
        print(f"Service {service_id} registered")
    
    def deregister_service(self, service_id):
        """Désenregistrer un service"""
        self.consul.agent.service.deregister(service_id)
        print(f"Service {service_id} deregistered")
    
    def discover_service(self, service_name):
        """Découvrir instances d'un service"""
        _, services = self.consul.health.service(service_name, passing=True)
        return [
            {
                'host': s['Service']['Address'],
                'port': s['Service']['Port']
            }
            for s in services
        ]

# Utilisation dans service
registry = ServiceRegistry()

# Enregistrer au démarrage
registry.register_service(
    service_name='users-service',
    service_id='users-service-1',
    host='localhost',
    port=8001
)

# Découvrir service
instances = registry.discover_service('orders-service')
if instances:
    instance = instances[0]  # ou load balancing
    url = f"http://{instance['host']}:{instance['port']}"


# === ETCD ===
pip install etcd3

import etcd3

etcd = etcd3.client(host='localhost', port=2379)

# Enregistrer service
etcd.put(
    '/services/users-service/instance-1',
    'http://localhost:8001',
    lease=etcd.lease(ttl=30)
)

# Découvrir services
services = etcd.get_prefix('/services/users-service/')
for value, metadata in services:
    print(f"Service instance: {value.decode()}")


# === NGINX LOAD BALANCING ===
# nginx.conf
upstream users_service {
    least_conn;
    server localhost:8001;
    server localhost:8002;
    server localhost:8003;
}

server {
    listen 80;
    
    location /users {
        proxy_pass http://users_service;
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
    }
}


[OK] API GATEWAY

# === KONG (POPULAIRE) ===
# docker-compose.yml
version: '3'
services:
  kong-database:
    image: postgres:13
    environment:
      POSTGRES_USER: kong
      POSTGRES_DB: kong
      POSTGRES_PASSWORD: kong
  
  kong:
    image: kong:latest
    environment:
      KONG_DATABASE: postgres
      KONG_PG_HOST: kong-database
      KONG_PROXY_ACCESS_LOG: /dev/stdout
      KONG_ADMIN_ACCESS_LOG: /dev/stdout
    ports:
      - "8000:8000"
      - "8443:8443"
      - "8001:8001"

# Ajouter service
curl -i -X POST http://localhost:8001/services/ \
  --data 'name=users-service' \
  --data 'url=http://users-service:8001'

# Ajouter route
curl -i -X POST http://localhost:8001/services/users-service/routes \
  --data 'paths[]=/users'

# Ajouter plugin rate limiting
curl -i -X POST http://localhost:8001/services/users-service/plugins \
  --data 'name=rate-limiting' \
  --data 'config.minute=100'


# === CUSTOM API GATEWAY (FASTAPI) ===
pip install fastapi httpx

# gateway.py
from fastapi import FastAPI, HTTPException, Request
from fastapi.responses import Response
import httpx
from typing import Dict

app = FastAPI(title="API Gateway")

# Configuration des services
SERVICES = {
    "users": "http://localhost:8001",
    "orders": "http://localhost:8002",
    "products": "http://localhost:8003",
}

async def forward_request(service_url: str, path: str, request: Request):
    """Transférer la requête au service"""
    async with httpx.AsyncClient() as client:
        # Construire URL complète
        url = f"{service_url}{path}"
        
        # Copier headers
        headers = dict(request.headers)
        headers.pop('host', None)
        
        # Transférer selon méthode
        if request.method == "GET":
            response = await client.get(url, headers=headers)
        elif request.method == "POST":
            body = await request.body()
            response = await client.post(url, headers=headers, content=body)
        elif request.method == "PUT":
            body = await request.body()
            response = await client.put(url, headers=headers, content=body)
        elif request.method == "DELETE":
            response = await client.delete(url, headers=headers)
        else:
            raise HTTPException(status_code=405, detail="Method not allowed")
        
        return Response(
            content=response.content,
            status_code=response.status_code,
            headers=dict(response.headers)
        )

@app.api_route("/users/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def users_gateway(path: str, request: Request):
    return await forward_request(SERVICES["users"], f"/users/{path}", request)

@app.api_route("/orders/{path:path}", methods=["GET", "POST", "PUT", "DELETE"])
async def orders_gateway(path: str, request: Request):
    return await forward_request(SERVICES["orders"], f"/orders/{path}", request)

@app.get("/health")
async def health():
    return {"status": "healthy", "services": SERVICES}

if __name__ == "__main__":
    import uvicorn
    uvicorn.run(app, host="0.0.0.0", port=8000)


[OK] AUTHENTIFICATION & AUTORISATION

# === JWT AUTHENTICATION ===
pip install pyjwt python-jose passlib

# auth.py
from datetime import datetime, timedelta
from jose import JWTError, jwt
from passlib.context import CryptContext
from fastapi import HTTPException, Security
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials

SECRET_KEY = "your-secret-key-change-in-production"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
security = HTTPBearer()

def create_access_token(data: dict):
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

def verify_token(credentials: HTTPAuthorizationCredentials = Security(security)):
    try:
        token = credentials.credentials
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        user_id: str = payload.get("sub")
        if user_id is None:
            raise HTTPException(status_code=401, detail="Invalid token")
        return payload
    except JWTError:
        raise HTTPException(status_code=401, detail="Invalid token")

# Dans service
from fastapi import Depends

@app.post("/login")
async def login(username: str, password: str):
    # Vérifier credentials (simplifié)
    if username == "admin" and password == "password":
        token = create_access_token(data={"sub": username, "role": "admin"})
        return {"access_token": token, "token_type": "bearer"}
    raise HTTPException(status_code=401, detail="Invalid credentials")

@app.get("/users/me")
async def get_current_user(token_data = Depends(verify_token)):
    return {"user": token_data["sub"], "role": token_data.get("role")}


# === OAUTH2 / SSO ===
pip install authlib

from authlib.integrations.starlette_client import OAuth
from starlette.config import Config

config = Config('.env')
oauth = OAuth(config)

oauth.register(
    name='google',
    client_id=config('GOOGLE_CLIENT_ID'),
    client_secret=config('GOOGLE_CLIENT_SECRET'),
    server_metadata_url='https://accounts.google.com/.well-known/openid-configuration',
    client_kwargs={'scope': 'openid email profile'}
)

@app.get('/login/google')
async def login_google(request: Request):
    redirect_uri = request.url_for('auth_google')
    return await oauth.google.authorize_redirect(request, redirect_uri)

@app.get('/auth/google')
async def auth_google(request: Request):
    token = await oauth.google.authorize_access_token(request)
    user = token.get('userinfo')
    # Créer session utilisateur
    return user


[OK] CIRCUIT BREAKER PATTERN

pip install pybreaker

# circuit_breaker.py
import pybreaker
import httpx

# Créer circuit breaker
breaker = pybreaker.CircuitBreaker(
    fail_max=5,              # Ouvrir après 5 échecs
    timeout_duration=60,     # Rester ouvert 60 secondes
    expected_exception=Exception
)

class ResilientServiceClient:
    def __init__(self, base_url):
        self.base_url = base_url
    
    @breaker
    async def get_user(self, user_id):
        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"{self.base_url}/users/{user_id}",
                timeout=5.0
            )
            response.raise_for_status()
            return response.json()
    
    async def get_user_with_fallback(self, user_id):
        try:
            return await self.get_user(user_id)
        except pybreaker.CircuitBreakerError:
            # Circuit ouvert, retourner cache ou valeur par défaut
            return {"id": user_id, "name": "Unknown", "from_cache": True}
        except Exception as e:
            # Autre erreur
            return {"error": str(e)}

# Utilisation
client = ResilientServiceClient("http://localhost:8001")
user = await client.get_user_with_fallback(123)


# Circuit Breaker avec Tenacity
pip install tenacity

from tenacity import retry, stop_after_attempt, wait_exponential

class RetryServiceClient:
    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=2, max=10)
    )
    async def get_user(self, user_id):
        async with httpx.AsyncClient() as client:
            response = await client.get(
                f"http://localhost:8001/users/{user_id}",
                timeout=5.0
            )
            response.raise_for_status()
            return response.json()


[OK] DISTRIBUTED TRACING

# === JAEGER ===
pip install opentelemetry-api opentelemetry-sdk opentelemetry-instrumentation-fastapi
pip install opentelemetry-exporter-jaeger

# tracing.py
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.jaeger.thrift import JaegerExporter
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor

def setup_tracing(app, service_name):
    # Configurer provider
    trace.set_tracer_provider(TracerProvider())
    tracer_provider = trace.get_tracer_provider()
    
    # Configurer exporter Jaeger
    jaeger_exporter = JaegerExporter(
        agent_host_name="localhost",
        agent_port=6831,
    )
    
    tracer_provider.add_span_processor(BatchSpanProcessor(jaeger_exporter))
    
    # Instrumenter FastAPI
    FastAPIInstrumentor.instrument_app(app)
    
    return trace.get_tracer(service_name)

# main.py
from fastapi import FastAPI
from tracing import setup_tracing

app = FastAPI(title="Users Service")
tracer = setup_tracing(app, "users-service")

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    with tracer.start_as_current_span("get_user_from_db"):
        # Logique métier
        return {"id": user_id, "name": "John Doe"}


# === PROMETHEUS METRICS ===
pip install prometheus-client prometheus-fastapi-instrumentator

# metrics.py
from prometheus_client import Counter, Histogram, Gauge
from prometheus_fastapi_instrumentator import Instrumentator

# Métriques personnalisées
request_count = Counter(
    'service_requests_total',
    'Total service requests',
    ['method', 'endpoint', 'status']
)

request_duration = Histogram(
    'service_request_duration_seconds',
    'Request duration in seconds',
    ['method', 'endpoint']
)

active_users = Gauge(
    'active_users',
    'Number of active users'
)

# Instrumenter FastAPI
def setup_metrics(app):
    Instrumentator().instrument(app).expose(app)

# main.py
from fastapi import FastAPI
from metrics import setup_metrics, request_count

app = FastAPI()
setup_metrics(app)

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    request_count.labels(method="GET", endpoint="/users", status="200").inc()
    return {"id": user_id, "name": "John Doe"}

# Métriques disponibles sur: http://localhost:8001/metrics


[OK] LOGGING CENTRALISÉ

# === ELK STACK (Elasticsearch, Logstash, Kibana) ===
pip install python-logstash

# logging_config.py
import logging
import logstash
import sys

def setup_logging(service_name):
    logger = logging.getLogger(service_name)
    logger.setLevel(logging.INFO)
    
    # Console handler
    console_handler = logging.StreamHandler(sys.stdout)
    console_handler.setLevel(logging.INFO)
    formatter = logging.Formatter(
        '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    )
    console_handler.setFormatter(formatter)
    logger.addHandler(console_handler)
    
    # Logstash handler
    logstash_handler = logstash.TCPLogstashHandler(
        'localhost',
        5959,
        version=1
    )
    logger.addHandler(logstash_handler)
    
    return logger

# Utilisation
logger = setup_logging("users-service")
logger.info("User created", extra={
    "user_id": 123,
    "email": "user@example.com"
})


# === STRUCTURED LOGGING ===
pip install structlog

# structured_logging.py
import structlog

structlog.configure(
    processors=[
        structlog.stdlib.filter_by_level,
        structlog.stdlib.add_logger_name,
        structlog.stdlib.add_log_level,
        structlog.stdlib.PositionalArgumentsFormatter(),
        structlog.processors.TimeStamper(fmt="iso"),
        structlog.processors.StackInfoRenderer(),
        structlog.processors.format_exc_info,
        structlog.processors.UnicodeDecoder(),
        structlog.processors.JSONRenderer()
    ],
    context_class=dict,
    logger_factory=structlog.stdlib.LoggerFactory(),
    cache_logger_on_first_use=True,
)

logger = structlog.get_logger()

# Utilisation
logger.info("user_created", user_id=123, email="user@example.com")
logger.error("database_error", error="Connection failed", retries=3)


[OK] CONFIGURATION MANAGEMENT

# === ENVIRONMENT VARIABLES ===
pip install python-decouple

# config.py
from decouple import config

class Settings:
    # Service
    SERVICE_NAME = config('SERVICE_NAME', default='users-service')
    SERVICE_PORT = config('SERVICE_PORT', default=8001, cast=int)
    DEBUG = config('DEBUG', default=False, cast=bool)
    
    # Database
    DATABASE_URL = config('DATABASE_URL')
    DB_POOL_SIZE = config('DB_POOL_SIZE', default=10, cast=int)
    
    # Redis
    REDIS_URL = config('REDIS_URL', default='redis://localhost:6379/0')
    
    # External Services
    ORDERS_SERVICE_URL = config('ORDERS_SERVICE_URL')
    PRODUCTS_SERVICE_URL = config('PRODUCTS_SERVICE_URL')
    
    # Security
    SECRET_KEY = config('SECRET_KEY')
    JWT_ALGORITHM = config('JWT_ALGORITHM', default='HS256')
    JWT_EXPIRE_MINUTES = config('JWT_EXPIRE_MINUTES', default=30, cast=int)
    
    # Monitoring
    JAEGER_HOST = config('JAEGER_HOST', default='localhost')
    JAEGER_PORT = config('JAEGER_PORT', default=6831, cast=int)

settings = Settings()

# .env
SERVICE_NAME=users-service
SERVICE_PORT=8001
DATABASE_URL=postgresql://user:pass@localhost/users
ORDERS_SERVICE_URL=http://localhost:8002
SECRET_KEY=your-secret-key


# === PYDANTIC SETTINGS ===
pip install pydantic-settings

# config.py
from pydantic_settings import BaseSettings
from typing import Optional

class Settings(BaseSettings):
    # Service
    service_name: str = "users-service"
    service_port: int = 8001
    debug: bool = False
    
    # Database
    database_url: str
    db_pool_size: int = 10
    
    # Redis
    redis_url: str = "redis://localhost:6379/0"
    
    # External Services
    orders_service_url: str
    products_service_url: str
    
    # Security
    secret_key: str
    jwt_algorithm: str = "HS256"
    jwt_expire_minutes: int = 30
    
    class Config:
        env_file = ".env"
        env_file_encoding = "utf-8"

settings = Settings()


# === CONSUL KV STORE ===
pip install python-consul

# config_consul.py
import consul
import json

class ConsulConfig:
    def __init__(self, host='localhost', port=8500):
        self.consul = consul.Consul(host=host, port=port)
    
    def get_config(self, key):
        """Récupérer configuration"""
        _, data = self.consul.kv.get(key)
        if data:
            return json.loads(data['Value'].decode())
        return None
    
    def set_config(self, key, value):
        """Définir configuration"""
        self.consul.kv.put(key, json.dumps(value))
    
    def watch_config(self, key, callback):
        """Observer changements configuration"""
        index = None
        while True:
            index, data = self.consul.kv.get(key, index=index)
            if data:
                config = json.loads(data['Value'].decode())
                callback(config)

# Utilisation
config_store = ConsulConfig()
config_store.set_config('users-service/config', {
    'db_pool_size': 10,
    'cache_ttl': 300
})

config = config_store.get_config('users-service/config')


[OK] GESTION DES DONNÉES

# === DATABASE PER SERVICE ===

# Service Users - PostgreSQL
pip install asyncpg sqlalchemy

# users_service/database.py
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy import Column, Integer, String, Boolean

DATABASE_URL = "postgresql+asyncpg://user:pass@localhost/users"

engine = create_async_engine(DATABASE_URL, echo=True)
async_session = sessionmaker(
    engine, class_=AsyncSession, expire_on_commit=False
)
Base = declarative_base()

class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True)
    name = Column(String(100))
    email = Column(String(100), unique=True)
    active = Column(Boolean, default=True)

async def get_db():
    async with async_session() as session:
        yield session

# users_service/main.py
from fastapi import FastAPI, Depends
from sqlalchemy.ext.asyncio import AsyncSession
from database import get_db, User, engine, Base

app = FastAPI()

@app.on_event("startup")
async def startup():
    async with engine.begin() as conn:
        await conn.run_sync(Base.metadata.create_all)

@app.post("/users")
async def create_user(name: str, email: str, db: AsyncSession = Depends(get_db)):
    user = User(name=name, email=email)
    db.add(user)
    await db.commit()
    await db.refresh(user)
    return {"id": user.id, "name": user.name, "email": user.email}


# Service Orders - MongoDB
pip install motor

# orders_service/database.py
from motor.motor_asyncio import AsyncIOMotorClient
from bson import ObjectId

MONGODB_URL = "mongodb://localhost:27017"
client = AsyncIOMotorClient(MONGODB_URL)
database = client.orders_db
orders_collection = database.orders

async def create_order(order_data):
    result = await orders_collection.insert_one(order_data)
    return str(result.inserted_id)

async def get_order(order_id):
    order = await orders_collection.find_one({"_id": ObjectId(order_id)})
    if order:
        order["_id"] = str(order["_id"])
    return order


# === SAGA PATTERN (Transactions distribuées) ===

# saga.py
from enum import Enum
from typing import List, Callable
import asyncio

class SagaStep:
    def __init__(self, action: Callable, compensate: Callable):
        self.action = action
        self.compensate = compensate

class SagaOrchestrator:
    def __init__(self):
        self.steps: List[SagaStep] = []
        self.completed_steps: List[int] = []
    
    def add_step(self, action: Callable, compensate: Callable):
        self.steps.append(SagaStep(action, compensate))
    
    async def execute(self, context: dict):
        """Exécuter saga"""
        try:
            for idx, step in enumerate(self.steps):
                result = await step.action(context)
                context.update(result)
                self.completed_steps.append(idx)
            return context
        except Exception as e:
            # Erreur: compenser les étapes complétées
            await self.rollback(context)
            raise e
    
    async def rollback(self, context: dict):
        """Annuler les étapes complétées"""
        for idx in reversed(self.completed_steps):
            step = self.steps[idx]
            try:
                await step.compensate(context)
            except Exception as e:
                # Logger l'erreur de compensation
                print(f"Compensation failed for step {idx}: {e}")

# Exemple: Créer commande
async def reserve_inventory(context):
    # Appeler service inventaire
    print(f"Reserving inventory for order {context['order_id']}")
    return {"inventory_reserved": True}

async def cancel_inventory_reservation(context):
    print(f"Cancelling inventory reservation for order {context['order_id']}")

async def process_payment(context):
    print(f"Processing payment for order {context['order_id']}")
    return {"payment_processed": True}

async def refund_payment(context):
    print(f"Refunding payment for order {context['order_id']}")

async def create_shipment(context):
    print(f"Creating shipment for order {context['order_id']}")
    return {"shipment_created": True}

async def cancel_shipment(context):
    print(f"Cancelling shipment for order {context['order_id']}")

# Utilisation
async def create_order_saga(order_id):
    saga = SagaOrchestrator()
    saga.add_step(reserve_inventory, cancel_inventory_reservation)
    saga.add_step(process_payment, refund_payment)
    saga.add_step(create_shipment, cancel_shipment)
    
    context = {"order_id": order_id}
    result = await saga.execute(context)
    return result


# === EVENT SOURCING ===

# event_store.py
from datetime import datetime
from typing import List, Dict, Any
import json

class Event:
    def __init__(self, aggregate_id: str, event_type: str, data: Dict[str, Any]):
        self.aggregate_id = aggregate_id
        self.event_type = event_type
        self.data = data
        self.timestamp = datetime.utcnow()
        self.version = 1

class EventStore:
    def __init__(self):
        self.events: List[Event] = []
    
    def append_event(self, event: Event):
        """Ajouter événement"""
        self.events.append(event)
    
    def get_events(self, aggregate_id: str) -> List[Event]:
        """Récupérer tous les événements d'un agrégat"""
        return [e for e in self.events if e.aggregate_id == aggregate_id]
    
    def get_events_by_type(self, event_type: str) -> List[Event]:
        """Récupérer événements par type"""
        return [e for e in self.events if e.event_type == event_type]

# Aggregate
class Order:
    def __init__(self, order_id: str):
        self.order_id = order_id
        self.status = "created"
        self.items = []
        self.total = 0.0
    
    def apply_event(self, event: Event):
        """Appliquer événement pour reconstruire état"""
        if event.event_type == "OrderCreated":
            self.status = "created"
        elif event.event_type == "ItemAdded":
            self.items.append(event.data["item"])
            self.total += event.data["price"]
        elif event.event_type == "OrderCompleted":
            self.status = "completed"
    
    @staticmethod
    def from_events(events: List[Event]) -> 'Order':
        """Reconstruire agrégat depuis événements"""
        if not events:
            return None
        
        order = Order(events[0].aggregate_id)
        for event in events:
            order.apply_event(event)
        return order

# Utilisation
event_store = EventStore()

# Créer commande
order_id = "order-123"
event_store.append_event(Event(order_id, "OrderCreated", {"customer_id": "user-1"}))
event_store.append_event(Event(order_id, "ItemAdded", {"item": "Product A", "price": 29.99}))
event_store.append_event(Event(order_id, "ItemAdded", {"item": "Product B", "price": 19.99}))
event_store.append_event(Event(order_id, "OrderCompleted", {}))

# Reconstruire état
events = event_store.get_events(order_id)
order = Order.from_events(events)
print(f"Order {order.order_id}: {order.status}, Total: ${order.total}")


# === CQRS (Command Query Responsibility Segregation) ===

# commands.py
from dataclasses import dataclass

@dataclass
class CreateUserCommand:
    name: str
    email: str

@dataclass
class UpdateUserCommand:
    user_id: int
    name: str
    email: str

# command_handler.py
class UserCommandHandler:
    def __init__(self, event_store, write_db):
        self.event_store = event_store
        self.write_db = write_db
    
    async def handle_create_user(self, command: CreateUserCommand):
        # Créer utilisateur dans write DB
        user_id = await self.write_db.create_user(command.name, command.email)
        
        # Publier événement
        event = Event(
            aggregate_id=str(user_id),
            event_type="UserCreated",
            data={"name": command.name, "email": command.email}
        )
        self.event_store.append_event(event)
        
        return user_id

# queries.py
@dataclass
class GetUserQuery:
    user_id: int

@dataclass
class ListUsersQuery:
    page: int = 1
    limit: int = 10

# query_handler.py
class UserQueryHandler:
    def __init__(self, read_db):
        self.read_db = read_db
    
    async def handle_get_user(self, query: GetUserQuery):
        # Lire depuis read DB (optimisée pour lecture)
        return await self.read_db.get_user(query.user_id)
    
    async def handle_list_users(self, query: ListUsersQuery):
        return await self.read_db.list_users(query.page, query.limit)


[OK] CACHE DISTRIBUÉ

# === REDIS ===
pip install redis aioredis

# cache.py
import redis.asyncio as redis
import json
from typing import Optional, Any

class RedisCache:
    def __init__(self, redis_url: str = "redis://localhost:6379"):
        self.redis = redis.from_url(redis_url)
    
    async def get(self, key: str) -> Optional[Any]:
        """Récupérer valeur du cache"""
        value = await self.redis.get(key)
        if value:
            return json.loads(value)
        return None
    
    async def set(self, key: str, value: Any, ttl: int = 300):
        """Stocker valeur dans cache avec TTL"""
        await self.redis.setex(
            key,
            ttl,
            json.dumps(value)
        )
    
    async def delete(self, key: str):
        """Supprimer valeur du cache"""
        await self.redis.delete(key)
    
    async def exists(self, key: str) -> bool:
        """Vérifier si clé existe"""
        return await self.redis.exists(key)
    
    async def increment(self, key: str) -> int:
        """Incrémenter compteur"""
        return await self.redis.incr(key)
    
    async def set_hash(self, key: str, mapping: dict):
        """Stocker hash"""
        await self.redis.hset(key, mapping=mapping)
    
    async def get_hash(self, key: str) -> dict:
        """Récupérer hash"""
        return await self.redis.hgetall(key)

# Utilisation dans service
from cache import RedisCache

cache = RedisCache()

@app.get("/users/{user_id}")
async def get_user(user_id: int):
    # Vérifier cache
    cache_key = f"user:{user_id}"
    cached_user = await cache.get(cache_key)
    
    if cached_user:
        return cached_user
    
    # Récupérer de la DB
    user = await db.get_user(user_id)
    
    # Mettre en cache
    await cache.set(cache_key, user, ttl=300)
    
    return user


# === CACHE DECORATOR ===
from functools import wraps
import hashlib

def cached(ttl: int = 300):
    def decorator(func):
        @wraps(func)
        async def wrapper(*args, **kwargs):
            # Générer clé cache
            cache_key = f"{func.__name__}:{hashlib.md5(str(args).encode() + str(kwargs).encode()).hexdigest()}"
            
            # Vérifier cache
            cached_value = await cache.get(cache_key)
            if cached_value:
                return cached_value
            
            # Exécuter fonction
            result = await func(*args, **kwargs)
            
            # Mettre en cache
            await cache.set(cache_key, result, ttl=ttl)
            
            return result
        return wrapper
    return decorator

# Utilisation
@cached(ttl=600)
async def get_user_orders(user_id: int):
    # Requête coûteuse
    return await db.query(f"SELECT * FROM orders WHERE user_id = {user_id}")


[OK] CONTAINERISATION & ORCHESTRATION

# === DOCKER ===

# Dockerfile (FastAPI service)
FROM python:3.11-slim

WORKDIR /app

# Copier requirements
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copier code
COPY . .

# Exposer port
EXPOSE 8001

# Healthcheck
HEALTHCHECK --interval=30s --timeout=3s \
  CMD python -c "import requests; requests.get('http://localhost:8001/health')"

# Commande de démarrage
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8001"]

# requirements.txt
fastapi==0.104.1
uvicorn[standard]==0.24.0
sqlalchemy==2.0.23
asyncpg==0.29.0
redis==5.0.1
pydantic-settings==2.1.0
python-jose[cryptography]==3.3.0
passlib[bcrypt]==1.7.4

# .dockerignore
__pycache__
*.pyc
*.pyo
*.pyd
.Python
env/
venv/
.venv/
.git
.gitignore
README.md
tests/
.pytest_cache


# docker-compose.yml (Architecture complète)
version: '3.8'

services:
  # API Gateway
  gateway:
    build: ./gateway
    ports:
      - "8000:8000"
    environment:
      - USERS_SERVICE_URL=http://users:8001
      - ORDERS_SERVICE_URL=http://orders:8002
    depends_on:
      - users
      - orders
    networks:
      - microservices

  # Service Users
  users:
    build: ./users-service
    ports:
      - "8001:8001"
    environment:
      - DATABASE_URL=postgresql://user:pass@postgres:5432/users
      - REDIS_URL=redis://redis:6379/0
    depends_on:
      - postgres
      - redis
    networks:
      - microservices

  # Service Orders
  orders:
    build: ./orders-service
    ports:
      - "8002:8002"
    environment:
      - MONGODB_URL=mongodb://mongo:27017
      - REDIS_URL=redis://redis:6379/1
    depends_on:
      - mongo
      - redis
    networks:
      - microservices

  # PostgreSQL
  postgres:
    image: postgres:15
    environment:
      - POSTGRES_USER=user
      - POSTGRES_PASSWORD=pass
      - POSTGRES_DB=users
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - microservices

  # MongoDB
  mongo:
    image: mongo:7
    volumes:
      - mongo_data:/data/db
    networks:
      - microservices

  # Redis
  redis:
    image: redis:7-alpine
    networks:
      - microservices

  # RabbitMQ
  rabbitmq:
    image: rabbitmq:3-management
    ports:
      - "5672:5672"
      - "15672:15672"
    environment:
      - RABBITMQ_DEFAULT_USER=admin
      - RABBITMQ_DEFAULT_PASS=admin
    networks:
      - microservices

  # Jaeger (Tracing)
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "6831:6831/udp"
      - "16686:16686"
    networks:
      - microservices

  # Prometheus (Metrics)
  prometheus:
    image: prom/prometheus
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml
    networks:
      - microservices

  # Grafana (Visualization)
  grafana:
    image: grafana/grafana
    ports:
      - "3000:3000"
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    networks:
      - microservices

networks:
  microservices:
    driver: bridge

volumes:
  postgres_data:
  mongo_data:

# Commandes Docker
# Build
docker-compose build

# Démarrer tous les services
docker-compose up -d

# Voir logs
docker-compose logs -f users

# Arrêter
docker-compose down

# Arrêter et supprimer volumes
docker-compose down -v


# === KUBERNETES ===

# users-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: users-service
  labels:
    app: users-service
spec:
  replicas: 3
  selector:
    matchLabels:
      app: users-service
  template:
    metadata:
      labels:
        app: users-service
    spec:
      containers:
      - name: users-service
        image: users-service:latest
        ports:
        - containerPort: 8001
        env:
        - name: DATABASE_URL
          valueFrom:
            secretKeyRef:
              name: db-secret
              key: database-url
        - name: REDIS_URL
          value: "redis://redis:6379/0"
        resources:
          requests:
            memory: "256Mi"
            cpu: "250m"
          limits:
            memory: "512Mi"
            cpu: "500m"
        livenessProbe:
          httpGet:
            path: /health
            port: 8001
          initialDelaySeconds: 30
          periodSeconds: 10
        readinessProbe:
          httpGet:
            path: /health
            port: 8001
          initialDelaySeconds: 5
          periodSeconds: 5

---
apiVersion: v1
kind: Service
metadata:
  name: users-service
spec:
  selector:
    app: users-service
  ports:
  - protocol: TCP
    port: 8001
    targetPort: 8001
  type: ClusterIP

---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
  name: users-service-hpa
spec:
  scaleTargetRef:
    apiVersion: apps/v1
    kind: Deployment
    name: users-service
  minReplicas: 3
  maxReplicas: 10
  metrics:
  - type: Resource
    resource:
      name: cpu
      target:
        type: Utilization
        averageUtilization: 70
  - type: Resource
    resource:
      name: memory
      target:
        type: Utilization
        averageUtilization: 80

# Secrets
# db-secret.yaml
apiVersion: v1
kind: Secret
metadata:
  name: db-secret
type: Opaque
stringData:
  database-url: "postgresql://user:pass@postgres:5432/users"

# ConfigMap
# users-config.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: users-config
data:
  LOG_LEVEL: "INFO"
  CACHE_TTL: "300"

# Ingress
# ingress.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: microservices-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  rules:
  - host: api.example.com
    http:
      paths:
      - path: /users
        pathType: Prefix
        backend:
          service:
            name: users-service
            port:
              number: 8001
      - path: /orders
        pathType: Prefix
        backend:
          service:
            name: orders-service
            port:
              number: 8002

# Commandes Kubernetes
# Appliquer configuration
kubectl apply -f users-deployment.yaml
kubectl apply -f db-secret.yaml
kubectl apply -f users-config.yaml
kubectl apply -f ingress.yaml

# Voir pods
kubectl get pods

# Voir logs
kubectl logs -f users-service-xxxx

# Scaler manuellement
kubectl scale deployment users-service --replicas=5

# Voir HPA
kubectl get hpa

# Décrire service
kubectl describe service users-service


[OK] TESTS

# === TESTS UNITAIRES ===
pip install pytest pytest-asyncio pytest-cov httpx

# tests/test_users.py
import pytest
from httpx import AsyncClient
from main import app

@pytest.mark.asyncio
async def test_create_user():
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.post("/users", json={
            "name": "John Doe",
            "email": "john@example.com"
        })
        assert response.status_code == 201
        data = response.json()
        assert data["name"] == "John Doe"
        assert data["email"] == "john@example.com"

@pytest.mark.asyncio
async def test_get_user():
    async with AsyncClient(app=app, base_url="http://test") as client:
        # Créer utilisateur
        create_response = await client.post("/users", json={
            "name": "Jane Doe",
            "email": "jane@example.com"
        })
        user_id = create_response.json()["id"]
        
        # Récupérer utilisateur
        response = await client.get(f"/users/{user_id}")
        assert response.status_code == 200
        data = response.json()
        assert data["name"] == "Jane Doe"

@pytest.mark.asyncio
async def test_user_not_found():
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.get("/users/9999")
        assert response.status_code == 404


# === TESTS D'INTÉGRATION ===

# tests/test_integration.py
import pytest
import asyncio
from httpx import AsyncClient

@pytest.mark.asyncio
async def test_order_creation_workflow():
    """Test workflow complet de création de commande"""
    
    # 1. Créer utilisateur
    async with AsyncClient(base_url="http://localhost:8001") as users_client:
        user_response = await users_client.post("/users", json={
            "name": "Test User",
            "email": "test@example.com"
        })
        assert user_response.status_code == 201
        user_id = user_response.json()["id"]
    
    # 2. Créer commande
    async with AsyncClient(base_url="http://localhost:8002") as orders_client:
        order_response = await orders_client.post("/orders", json={
            "user_id": user_id,
            "items": [
                {"product_id": 1, "quantity": 2},
                {"product_id": 2, "quantity": 1}
            ]
        })
        assert order_response.status_code == 201
        order_id = order_response.json()["id"]
    
    # 3. Vérifier commande créée
    async with AsyncClient(base_url="http://localhost:8002") as orders_client:
        get_response = await orders_client.get(f"/orders/{order_id}")
        assert get_response.status_code == 200
        order_data = get_response.json()
        assert order_data["user_id"] == user_id
        assert len(order_data["items"]) == 2


# === MOCKING ===
pip install pytest-mock

# tests/test_with_mocks.py
import pytest
from unittest.mock import AsyncMock, patch

@pytest.mark.asyncio
async def test_get_user_with_external_service_mock(mocker):
    """Test avec mock de service externe"""
    
    # Mock du client HTTP
    mock_orders = AsyncMock(return_value={
        "orders": [
            {"id": 1, "total": 100.00},
            {"id": 2, "total": 50.00}
        ]
    })
    
    mocker.patch('services.orders_client.get_user_orders', mock_orders)
    
    # Tester
    async with AsyncClient(app=app, base_url="http://test") as client:
        response = await client.get("/users/1/orders")
        assert response.status_code == 200
        data = response.json()
        assert len(data["orders"]) == 2


# === TESTS DE CHARGE ===
pip install locust

# locustfile.py
from locust import HttpUser, task, between

class MicroserviceUser(HttpUser):
    wait_time = between(1, 3)
    
    @task(3)
    def get_users(self):
        """Récupérer liste utilisateurs"""
        self.client.get("/users")
    
    @task(2)
    def get_user(self):
        """Récupérer utilisateur spécifique"""
        user_id = 1
        self.client.get(f"/users/{user_id}")
    
    @task(1)
    def create_user(self):
        """Créer utilisateur"""
        self.client.post("/users", json={
            "name": "Test User",
            "email": f"user{self.environment.stats.num_requests}@example.com"
        })
    
    def on_start(self):
        """Exécuté au démarrage de chaque utilisateur"""
        # Login si nécessaire
        response = self.client.post("/login", json={
            "username": "test",
            "password": "test123"
        })
        if response.status_code == 200:
            self.token = response.json()["access_token"]

# Lancer test de charge
# locust -f locustfile.py --host=http://localhost:8001
# Interface web: http://localhost:8089


# === CONTRACT Testing (PACT) ===
pip install pact-python

# tests/test_pact_consumer.py
import pytest
from pact import Consumer, Provider

@pytest.fixture
def pact():
    pact = Consumer('OrdersService').has_pact_with(Provider('UsersService'))
    pact.start_service()
    yield pact
    pact.stop_service()

def test_get_user_contract(pact):
    """Test contrat entre OrdersService et UsersService"""
    expected = {
        'id': 1,
        'name': 'John Doe',
        'email': 'john@example.com'
    }
    
    (pact
     .given('user 1 exists')
     .upon_receiving('a request for user 1')
     .with_request('GET', '/users/1')
     .will_respond_with(200, body=expected))
    
    with pact:
        # Faire la requête
        result = requests.get(f'{pact.uri}/users/1')
        assert result.json() == expected


[OK] CI/CD

# === GITHUB ACTIONS ===

# .github/workflows/ci.yml
name: CI/CD Pipeline

on:
  push:
    branches: [ main, develop ]
  pull_request:
    branches: [ main ]

jobs:
  test:
    runs-on: ubuntu-latest
    
    services:
      postgres:
        image: postgres:15
        env:
          POSTGRES_USER: test
          POSTGRES_PASSWORD: test
          POSTGRES_DB: test
        options: >-
          --health-cmd pg_isready
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
      
      redis:
        image: redis:7-alpine
        options: >-
          --health-cmd "redis-cli ping"
          --health-interval 10s
          --health-timeout 5s
          --health-retries 5
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Python
      uses: actions/setup-python@v4
      with:
        python-version: '3.11'
    
    - name: Cache dependencies
      uses: actions/cache@v3
      with:
        path: ~/.cache/pip
        key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install -r requirements.txt
        pip install -r requirements-test.txt
    
    - name: Run linting
      run: |
        pip install flake8 black
        flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
        black --check .
    
    - name: Run tests
      env:
        DATABASE_URL: postgresql://test:test@localhost:5432/test
        REDIS_URL: redis://localhost:6379/0
      run: |
        pytest --cov=. --cov-report=xml --cov-report=html
    
    - name: Upload coverage
      uses: codecov/codecov-action@v3
      with:
        files: ./coverage.xml
  
  build:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - uses: actions/checkout@v3
    
    - name: Set up Docker Buildx
      uses: docker/setup-buildx-action@v2
    
    - name: Login to DockerHub
      uses: docker/login-action@v2
      with:
        username: ${{ secrets.DOCKER_USERNAME }}
        password: ${{ secrets.DOCKER_PASSWORD }}
    
    - name: Build and push
      uses: docker/build-push-action@v4
      with:
        context: .
        push: true
        tags: |
          myorg/users-service:latest
          myorg/users-service:${{ github.sha }}
        cache-from: type=gha
        cache-to: type=gha,mode=max
  
  deploy:
    needs: build
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    
    steps:
    - name: Deploy to Kubernetes
      uses: azure/k8s-deploy@v4
      with:
        manifests: |
          k8s/deployment.yaml
          k8s/service.yaml
        images: |
          myorg/users-service:${{ github.sha }}
        kubectl-version: 'latest'


# === GITLAB CI ===

# .gitlab-ci.yml
stages:
  - test
  - build
  - deploy

variables:
  DOCKER_DRIVER: overlay2
  DOCKER_TLS_CERTDIR: ""

test:
  stage: test
  image: python:3.11
  services:
    - postgres:15
    - redis:7-alpine
  variables:
    POSTGRES_DB: test
    POSTGRES_USER: test
    POSTGRES_PASSWORD: test
    DATABASE_URL: postgresql://test:test@postgres:5432/test
    REDIS_URL: redis://redis:6379/0
  before_script:
    - pip install -r requirements.txt
    - pip install -r requirements-test.txt
  script:
    - flake8 .
    - black --check .
    - pytest --cov=. --cov-report=term --cov-report=html
  coverage: '/TOTAL.*\s+(\d+%)$/'
  artifacts:
    reports:
      coverage_report:
        coverage_format: cobertura
        path: coverage.xml

build:
  stage: build
  image: docker:latest
  services:
    - docker:dind
  only:
    - main
  script:
    - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
    - docker build -t $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA .
    - docker tag $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA $CI_REGISTRY_IMAGE:latest
    - docker push $CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - docker push $CI_REGISTRY_IMAGE:latest

deploy_staging:
  stage: deploy
  image: bitnami/kubectl:latest
  only:
    - main
  script:
    - kubectl config use-context staging
    - kubectl set image deployment/users-service users-service=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - kubectl rollout status deployment/users-service

deploy_production:
  stage: deploy
  image: bitnami/kubectl:latest
  only:
    - main
  when: manual
  script:
    - kubectl config use-context production
    - kubectl set image deployment/users-service users-service=$CI_REGISTRY_IMAGE:$CI_COMMIT_SHA
    - kubectl rollout status deployment/users-service


[OK] SÉCURITÉ

# === SECRETS MANAGEMENT ===

# Vault (HashiCorp)
pip install hvac

# vault_client.py
import hvac

class VaultClient:
    def __init__(self, url='http://localhost:8200', token=None):
        self.client = hvac.Client(url=url, token=token)
    
    def get_secret(self, path):
        """Récupérer secret"""
        response = self.client.secrets.kv.v2.read_secret_version(path=path)
        return response['data']['data']
    
    def set_secret(self, path, secret_data):
        """Définir secret"""
        self.client.secrets.kv.v2.create_or_update_secret(
            path=path,
            secret=secret_data
        )

# Utilisation
vault = VaultClient(token='your-token')
db_credentials = vault.get_secret('database/users-service')
DATABASE_URL = db_credentials['url']


# === RATE LIMITING ===
pip install slowapi

# rate_limit.py
from slowapi import Limiter, _rate_limit_exceeded_handler
from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded
from fastapi import FastAPI, Request

limiter = Limiter(key_func=get_remote_address)
app = FastAPI()
app.state.limiter = limiter
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)

@app.get("/users")
@limiter.limit("5/minute")
async def list_users(request: Request):
    return {"users": []}

@app.post("/users")
@limiter.limit("10/hour")
async def create_user(request: Request, user_data: dict):
    return {"id": 1, **user_data}


# === INPUT VALIDATION ===
from pydantic import BaseModel, EmailStr, validator, Field
from typing import Optional

class UserCreate(BaseModel):
    name: str = Field(..., min_length=2, max_length=100)
    email: EmailStr
    age: Optional[int] = Field(None, ge=0, le=150)
    
    @validator('name')
    def name_must_not_contain_special_chars(cls, v):
        if not v.replace(' ', '').isalnum():
            raise ValueError('name must be alphanumeric')
        return v

@app.post("/users")
async def create_user(user: UserCreate):
    # user est automatiquement validé
    return {"id": 1, **user.dict()}


# === SQL INJECTION PREVENTION ===
# [OK] Utiliser ORM (SQLAlchemy)
# [OK] Requêtes paramétrées

# MAUVAIS
query = f"SELECT * FROM users WHERE id = {user_id}"  # VULNÉRABLE

# BON
from sqlalchemy import text
query = text("SELECT * FROM users WHERE id = :user_id")
result = await session.execute(query, {"user_id": user_id})


# === CORS ===
from fastapi.middleware.cors import CORSMiddleware

app.add_middleware(
    CORSMiddleware,
    allow_origins=["https://example.com"],  # Pas "*" en production
    allow_credentials=True,
    allow_methods=["GET", "POST", "PUT", "DELETE"],
    allow_headers=["*"],
)


# === HTTPS / TLS ===
# uvicorn avec SSL
uvicorn main:app \
    --host 0.0.0.0 \
    --port 443 \
    --ssl-keyfile=/path/to/key.pem \
    --ssl-certfile=/path/to/cert.pem


# === SECURITY HEADERS ===
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi import FastAPI
from starlette.middleware.httpsredirect import HTTPSRedirectMiddleware

app = FastAPI()

# Forcer HTTPS
app.add_middleware(HTTPSRedirectMiddleware)

# Trusted hosts
app.add_middleware(
    TrustedHostMiddleware,
    allowed_hosts=["example.com", "*.example.com"]
)

@app.middleware("http")
async def add_security_headers(request, call_next):
    response = await call_next(request)
    response.headers["X-Content-Type-Options"] = "nosniff"
    response.headers["X-Frame-Options"] = "DENY"
    response.headers["X-XSS-Protection"] = "1; mode=block"
    response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains"
    return response


[OK] MONITORING & OBSERVABILITY

# === HEALTH CHECKS ===

# health.py
from fastapi import FastAPI, status
from typing import Dict
import asyncio

app = FastAPI()

class HealthChecker:
    def __init__(self):
        self.checks = {}
    
    def register_check(self, name: str, check_func):
        """Enregistrer un health check"""
        self.checks[name] = check_func
    
    async def run_checks(self) -> Dict:
        """Exécuter tous les checks"""
        results = {}
        overall_status = "healthy"
        
        for name, check_func in self.checks.items():
            try:
                result = await check_func()
                results[name] = {"status": "healthy", **result}
            except Exception as e:
                results[name] = {"status": "unhealthy", "error": str(e)}
                overall_status = "unhealthy"
        
        return {
            "status": overall_status,
            "checks": results
        }

health_checker = HealthChecker()

# Checks spécifiques
async def check_database():
    """Vérifier connexion base de données"""
    try:
        await db.execute("SELECT 1")
        return {"message": "Database connection OK"}
    except Exception as e:
        raise Exception(f"Database error: {e}")

async def check_redis():
    """Vérifier connexion Redis"""
    try:
        await redis.ping()
        return {"message": "Redis connection OK"}
    except Exception as e:
        raise Exception(f"Redis error: {e}")

async def check_external_service():
    """Vérifier service externe"""
    try:
        async with httpx.AsyncClient() as client:
            response = await client.get(
                "http://orders-service:8002/health",
                timeout=5.0
            )
            response.raise_for_status()
        return {"message": "Orders service OK"}
    except Exception as e:
        raise Exception(f"Orders service error: {e}")

# Enregistrer checks
health_checker.register_check("database", check_database)
health_checker.register_check("redis", check_redis)
health_checker.register_check("orders_service", check_external_service)

@app.get("/health", status_code=status.HTTP_200_OK)
async def health_check():
    """Health check simple"""
    return {"status": "healthy"}

@app.get("/health/ready", status_code=status.HTTP_200_OK)
async def readiness_check():
    """Readiness check détaillé"""
    results = await health_checker.run_checks()
    
    if results["status"] == "unhealthy":
        return JSONResponse(
            status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
            content=results
        )
    
    return results

@app.get("/health/live", status_code=status.HTTP_200_OK)
async def liveness_check():
    """Liveness check (basique)"""
    return {"status": "alive"}


# === ALERTING ===

# alerts.py
import smtplib
from email.mime.text import MIMEText
import requests

class AlertManager:
    def __init__(self):
        self.handlers = []
    
    def register_handler(self, handler):
        self.handlers.append(handler)
    
    async def send_alert(self, level: str, message: str, context: dict):
        """Envoyer alerte"""
        for handler in self.handlers:
            try:
                await handler.send(level, message, context)
            except Exception as e:
                print(f"Failed to send alert: {e}")

class EmailAlertHandler:
    def __init__(self, smtp_host, smtp_port, from_email, to_emails):
        self.smtp_host = smtp_host
        self.smtp_port = smtp_port
        self.from_email = from_email
        self.to_emails = to_emails
    
    async def send(self, level: str, message: str, context: dict):
        """Envoyer email"""
        subject = f"[{level.upper()}] Alert from {context.get('service')}"
        body = f"{message}\n\nContext: {context}"
        
        msg = MIMEText(body)
        msg['Subject'] = subject
        msg['From'] = self.from_email
        msg['To'] = ', '.join(self.to_emails)
        
        with smtplib.SMTP(self.smtp_host, self.smtp_port) as server:
            server.send_message(msg)

class SlackAlertHandler:
    def __init__(self, webhook_url):
        self.webhook_url = webhook_url
    
    async def send(self, level: str, message: str, context: dict):
        """Envoyer notification Slack"""
        color = {
            "critical": "#FF0000",
            "warning": "#FFA500",
            "info": "#00FF00"
        }.get(level, "#808080")
        
        payload = {
            "attachments": [{
                "color": color,
                "title": f"Alert: {level.upper()}",
                "text": message,
                "fields": [
                    {"title": key, "value": str(value), "short": True}
                    for key, value in context.items()
                ]
            }]
        }
        
        async with httpx.AsyncClient() as client:
            await client.post(self.webhook_url, json=payload)

# Utilisation
alert_manager = AlertManager()
alert_manager.register_handler(EmailAlertHandler(
    smtp_host="smtp.gmail.com",
    smtp_port=587,
    from_email="alerts@example.com",
    to_emails=["admin@example.com"]
))
alert_manager.register_handler(SlackAlertHandler(
    webhook_url="https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
))

# Envoyer alerte
await alert_manager.send_alert(
    level="critical",
    message="Database connection lost",
    context={"service": "users-service", "timestamp": datetime.now()}
)


[OK] PATTERNS AVANCÉS

# === RETRY PATTERN ===
from tenacity import (
    retry,
    stop_after_attempt,
    wait_exponential,
    retry_if_exception_type
)

class ServiceClient:
    @retry(
        stop=stop_after_attempt(3),
        wait=wait_exponential(multiplier=1, min=2, max=10),
        retry=retry_if_exception_type(httpx.RequestError)
    )
    async def call_service(self, url: str):
        async with httpx.AsyncClient() as client:
            response = await client.get(url, timeout=5.0)
            response.raise_for_status()
            return response.json()


# === BULKHEAD PATTERN ===
import asyncio
from asyncio import Semaphore

class BulkheadExecutor:
    def __init__(self, max_concurrent: int = 10):
        self.semaphore = Semaphore(max_concurrent)
    
    async def execute(self, coro):
        """Exécuter avec limite de concurrence"""
        async with self.semaphore:
            return await coro

# Utilisation
bulkhead = BulkheadExecutor(max_concurrent=5)

async def process_request(request_id):
    return await bulkhead.execute(
        some_expensive_operation(request_id)
    )


# === TIMEOUT PATTERN ===
import asyncio

async def call_with_timeout(coro, timeout: float):
    """Exécuter coroutine avec timeout"""
    try:
        return await asyncio.wait_for(coro, timeout=timeout)
    except asyncio.TimeoutError:
        raise Exception(f"Operation timed out after {timeout}s")

# Utilisation
result = await call_with_timeout(
    service_client.get_user(123),
    timeout=5.0
)


# === CACHE ASIDE PATTERN ===
async def get_user_with_cache(user_id: int):
    """Cache-aside pattern"""
    cache_key = f"user:{user_id}"
    
    # 1. Vérifier cache
    cached_user = await cache.get(cache_key)
    if cached_user:
        return cached_user
    
    # 2. Récupérer de la source
    user = await db.get_user(user_id)
    
    # 3. Mettre en cache
    if user:
        await cache.set(cache_key, user, ttl=300)
    
    return user


# === WRITE-THROUGH CACHE ===
async def update_user_write_through(user_id: int, user_data: dict):
    """Write-through cache pattern"""
    cache_key = f"user:{user_id}"
    
    # 1. Écrire en DB
    await db.update_user(user_id, user_data)
    
    # 2. Mettre à jour cache
    user = await db.get_user(user_id)
    await cache.set(cache_key, user, ttl=300)
    
    return user


# === WRITE-BEHIND CACHE ===
from asyncio import Queue

class WriteBehindCache:
    def __init__(self):
        self.queue = Queue()
        self.running = False
    
    async def write(self, key: str, value: dict):
        """Écrire dans cache et queue"""
        # Écrire dans cache immédiatement
        await cache.set(key, value)
        
        # Ajouter à queue pour écriture DB
        await self.queue.put((key, value))
    
    async def process_queue(self):
        """Traiter queue d'écriture"""
        self.running = True
        while self.running:
            try:
                key, value = await asyncio.wait_for(
                    self.queue.get(),
                    timeout=1.0
                )
                await db.write(key, value)
            except asyncio.TimeoutError:
                continue
    
    def stop(self):
        self.running = False


[OK] BONNES PRATIQUES

# === STRUCTURE PROJET RECOMMANDÉE ===

microservices/
├── api-gateway/
│   ├── src/
│   │   ├── main.py
│   │   ├── routes.py
│   │   └── middleware/
│   ├── tests/
│   ├── Dockerfile
│   └── requirements.txt
│
├── users-service/
│   ├── src/
│   │   ├── main.py
│   │   ├── models/
│   │   │   └── user.py
│   │   ├── repositories/
│   │   │   └── user_repository.py
│   │   ├── services/
│   │   │   └── user_service.py
│   │   ├── api/
│   │   │   └── v1/
│   │   │       └── users.py
│   │   ├── schemas/
│   │   │   └── user_schema.py
│   │   └── config/
│   │       ├── database.py
│   │       └── settings.py
│   ├── tests/
│   │   ├── unit/
│   │   ├── integration/
│   │   └── conftest.py
│   ├── alembic/  # Migrations
│   ├── Dockerfile
│   ├── requirements.txt
│   └── README.md
│
├── orders-service/
│   └── ... (structure similaire)
│
├── docker-compose.yml
├── kubernetes/
│   ├── namespace.yaml
│   ├── users-service/
│   │   ├── deployment.yaml
│   │   ├── service.yaml
│   │   └── configmap.yaml
│   └── orders-service/
│       └── ...
├── .github/
│   └── workflows/
│       └── ci-cd.yml
└── README.md


# === 12-FACTOR APP PRINCIPLES ===

# 1. Codebase: Un dépôt par service
# 2. Dependencies: requirements.txt explicites
# 3. Config: Variables d'environnement
# 4. Backing services: URLs configurables
# 5. Build, release, run: CI/CD strict
# 6. Processes: Stateless
# 7. Port binding: Service expose son port
# 8. Concurrency: Scale horizontal
# 9. Disposability: Démarrage/arrêt rapide
# 10. Dev/prod parity: Environnements similaires
# 11. Logs: Stdout/stderr
# 12. Admin processes: Scripts séparés


# === PRINCIPES DE CONCEPTION ===

# [OK] Single Responsibility Principle
#   - Un service = une responsabilité métier

# [OK] API First Design
#   - Définir API avant implémentation
#   - OpenAPI/Swagger

# [OK] Database per Service
#   - Chaque service sa BD
#   - Pas d'accès direct aux BD d'autres services

# [OK] Fail Fast
#   - Validation immédiate
#   - Erreurs explicites

# [OK] Idempotence
#   - Opérations peuvent être rejouées

# [OK] Backwards Compatibility
#   - API versioning
#   - Pas de breaking changes

# [OK] Observability
#   - Logs structurés
#   - Métriques
#   - Tracing distribué

# [OK] Resilience
#   - Circuit breakers
#   - Timeouts
#   - Retries
#   - Fallbacks


# === ANTI-PATTERNS À ÉVITER ===

# [X] Shared Database
#   Chaque service doit avoir sa propre DB

# [X] Distributed Monolith
#   Services trop couplés

# [X] Chatty Communication
#   Trop d'appels entre services

# [X] God Service
#   Service avec trop de responsabilités

# [X] Hardcoded URLs
#   Utiliser service discovery

# [X] Synchronous Chain
#   Éviter A -> B -> C -> D
#   Préférer événements asynchrones

# [X] No API Versioning
#   Toujours versionner les APIs

# [X] Missing Health Checks
#   Toujours implémenter /health

# [X] No Monitoring
#   Observability obligatoire

# [X] Shared Libraries with Business Logic
#   Partager uniquement utilitaires


[OK] MIGRATION MONOLITHE -> MICROSERVICES

# === STRATÉGIE STRANGLER FIG ===

# 1. Identifier bounded contexts
# 2. Extraire un service à la fois
# 3. Router trafic progressivement
# 4. Décommissionner code monolithe

# Exemple: Proxy pattern
# nginx.conf
upstream monolith {
    server monolith:8000;
}

upstream users_service {
    server users-service:8001;
}

server {
    listen 80;
    
    # Nouveau: microservice
    location /api/v2/users {
        proxy_pass http://users_service;
    }
    
    # Ancien: monolithe
    location / {
        proxy_pass http://monolith;
    }
}


# === ÉTAPES MIGRATION ===

# Phase 1: Preparation
# - Audit codebase
# - Identifier services
# - Définir APIs
# - Setup infrastructure

# Phase 2: Extraction
# - Créer premier microservice
# - Dupliquer données si nécessaire
# - Tests A/B

# Phase 3: Routing
# - Router nouveau trafic vers microservice
# - Garder monolithe pour compatibilité

# Phase 4: Data Sync
# - Synchroniser données entre systèmes
# - Event-driven sync

# Phase 5: Cutover
# - Rediriger tout le trafic vers microservice
# - Arrêter fonctionnalité dans monolithe

# Phase 6: Cleanup
# - Supprimer code du monolithe
# - Nettoyer dépendances


[OK] OUTILS & FRAMEWORKS COMPLÉMENTAIRES

# === FASTAPI EXTENSIONS ===
pip install fastapi-utils fastapi-pagination fastapi-cache2

# Pagination
from fastapi_pagination import Page, add_pagination, paginate

@app.get("/users", response_model=Page[User])
async def list_users():
    users = await db.get_all_users()
    return paginate(users)

add_pagination(app)


# Cache
from fastapi_cache import FastAPICache
from fastapi_cache.backends.redis import RedisBackend
from fastapi_cache.decorator import cache

@app.on_event("startup")
async def startup():
    redis = aioredis.from_url("redis://localhost")
    FastAPICache.init(RedisBackend(redis), prefix="fastapi-cache")

@app.get("/users/{user_id}")
@cache(expire=300)
async def get_user(user_id: int):
    return await db.get_user(user_id)


# === ASYNC TASK QUEUES ===

# Celery
pip install celery[redis]

# celery_app.py
from celery import Celery

celery_app = Celery(
    'tasks',
    broker='redis://localhost:6379/0',
    backend='redis://localhost:6379/0'
)

celery_app.conf.update(
    task_serializer='json',
    accept_content=['json'],
    result_serializer='json',
    timezone='UTC',
    enable_utc=True,
)

@celery_app.task(bind=True, max_retries=3)
def send_email_task(self, user_id, email, subject, body):
    try:
        # Logique d'envoi email
        print(f"Sending email to {email}")
        return {"status": "sent"}
    except Exception as e:
        # Retry avec backoff exponentiel
        raise self.retry(exc=e, countdown=60 * (2 ** self.request.retries))

# Dans service FastAPI
from celery_app import send_email_task

@app.post("/users")
async def create_user(user: UserCreate):
    # Créer utilisateur
    new_user = await db.create_user(user)
    
    # Tâche asynchrone
    send_email_task.delay(
        new_user.id,
        new_user.email,
        "Welcome!",
        "Welcome to our platform"
    )
    
    return new_user


# RQ (Simple Queue)
pip install rq

# worker.py
from redis import Redis
from rq import Queue

redis_conn = Redis(host='localhost', port=6379)
queue = Queue(connection=redis_conn)

def send_email(user_id, email, subject):
    print(f"Sending email to {email}")
    # Logique email

# Enqueue job
job = queue.enqueue(send_email, 1, 'user@example.com', 'Welcome')

# Worker
# rq worker


# === MESSAGE BROKERS ===

# NATS
pip install nats-py

# nats_client.py
import asyncio
from nats.aio.client import Client as NATS

async def publish_event():
    nc = NATS()
    await nc.connect("nats://localhost:4222")
    
    # Publier
    await nc.publish("user.created", b'{"user_id": 1}')
    
    await nc.close()

async def subscribe_events():
    nc = NATS()
    await nc.connect("nats://localhost:4222")
    
    async def message_handler(msg):
        subject = msg.subject
        data = msg.data.decode()
        print(f"Received on {subject}: {data}")
    
    # S'abonner
    await nc.subscribe("user.*", cb=message_handler)
    
    # Garder connexion
    await asyncio.sleep(3600)


# Apache Pulsar
pip install pulsar-client

# pulsar_client.py
import pulsar

client = pulsar.Client('pulsar://localhost:6650')

# Producer
producer = client.create_producer('user-events')
producer.send(('{"user_id": 1}').encode('utf-8'))

# Consumer
consumer = client.subscribe('user-events', 'my-subscription')

while True:
    msg = consumer.receive()
    print(f"Received: {msg.data()}")
    consumer.acknowledge(msg)


# === API DOCUMENTATION ===

# OpenAPI/Swagger personnalisé
from fastapi.openapi.utils import get_openapi

def custom_openapi():
    if app.openapi_schema:
        return app.openapi_schema
    
    openapi_schema = get_openapi(
        title="Users Service API",
        version="1.0.0",
        description="Microservice for user management",
        routes=app.routes,
    )
    
    openapi_schema["info"]["x-logo"] = {
        "url": "https://example.com/logo.png"
    }
    
    app.openapi_schema = openapi_schema
    return app.openapi_schema

app.openapi = custom_openapi


# ReDoc
from fastapi.responses import HTMLResponse

@app.get("/redoc", include_in_schema=False)
async def redoc_html():
    return get_redoc_html(
        openapi_url=app.openapi_url,
        title=app.title + " - ReDoc",
        redoc_js_url="/static/redoc.standalone.js",
    )


# === SERVICE MESH (ISTIO) ===

# VirtualService
# users-virtualservice.yaml
apiVersion: networking.istio.io/v1beta1
kind: VirtualService
metadata:
  name: users-service
spec:
  hosts:
  - users-service
  http:
  - match:
    - headers:
        version:
          exact: v2
    route:
    - destination:
        host: users-service
        subset: v2
  - route:
    - destination:
        host: users-service
        subset: v1
      weight: 90
    - destination:
        host: users-service
        subset: v2
      weight: 10  # Canary deployment

---
# DestinationRule
apiVersion: networking.istio.io/v1beta1
kind: DestinationRule
metadata:
  name: users-service
spec:
  host: users-service
  trafficPolicy:
    connectionPool:
      tcp:
        maxConnections: 100
      http:
        http1MaxPendingRequests: 50
        maxRequestsPerConnection: 2
    outlierDetection:
      consecutiveErrors: 5
      interval: 30s
      baseEjectionTime: 30s
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2


[OK] DEBUGGING & TROUBLESHOOTING

# === DISTRIBUTED DEBUGGING ===

# Correlation ID
import uuid
from contextvars import ContextVar

correlation_id_var: ContextVar[str] = ContextVar('correlation_id', default='')

@app.middleware("http")
async def correlation_middleware(request: Request, call_next):
    # Extraire ou générer correlation ID
    correlation_id = request.headers.get('X-Correlation-ID', str(uuid.uuid4()))
    correlation_id_var.set(correlation_id)
    
    # Ajouter à response
    response = await call_next(request)
    response.headers['X-Correlation-ID'] = correlation_id
    
    return response

# Logger avec correlation ID
logger.info(
    "User created",
    extra={"correlation_id": correlation_id_var.get(), "user_id": user_id}
)

# Propager aux autres services
async def call_other_service():
    headers = {"X-Correlation-ID": correlation_id_var.get()}
    async with httpx.AsyncClient() as client:
        response = await client.get(
            "http://orders-service:8002/orders",
            headers=headers
        )


# === REMOTE DEBUGGING ===

# debugpy pour PyCharm/VSCode
pip install debugpy

# main.py
import debugpy

if os.getenv("DEBUG_MODE") == "true":
    debugpy.listen(("0.0.0.0", 5678))
    print("Waiting for debugger...")
    debugpy.wait_for_client()

# Docker
# Dockerfile
EXPOSE 5678
ENV DEBUG_MODE=false


# === PROFILING ===

# cProfile
import cProfile
import pstats

def profile_endpoint():
    profiler = cProfile.Profile()
    profiler.enable()
    
    # Code à profiler
    result = expensive_operation()
    
    profiler.disable()
    stats = pstats.Stats(profiler)
    stats.sort_stats('cumulative')
    stats.print_stats(10)
    
    return result


# py-spy (production profiling)
# pip install py-spy
# py-spy record -o profile.svg --pid 12345


# === CHAOS ENGINEERING ===
pip install chaostoolkit chaostoolkit-kubernetes

# chaos.yaml
version: 1.0.0
title: Service resilience test
description: Test service behavior under failures

steady-state-hypothesis:
  title: Application is healthy
  probes:
  - type: probe
    name: users-service-is-available
    tolerance: 200
    provider:
      type: http
      url: http://users-service:8001/health

method:
- type: action
  name: terminate-random-pod
  provider:
    type: python
    module: chaosk8s.pod.actions
    func: terminate_pods
    arguments:
      label_selector: app=users-service
      rand: true
      ns: default

- type: probe
  name: service-still-available
  tolerance: 200
  provider:
    type: http
    url: http://users-service:8001/health

rollbacks:
- type: action
  name: scale-back-up
  provider:
    type: python
    module: chaosk8s.deployment.actions
    func: scale_deployment
    arguments:
      name: users-service
      replicas: 3
      ns: default

# Exécuter
# chaos run chaos.yaml


[OK] PERFORMANCE OPTIMIZATION

# === DATABASE OPTIMIZATION ===

# Connection pooling
from sqlalchemy.pool import QueuePool

engine = create_async_engine(
    DATABASE_URL,
    poolclass=QueuePool,
    pool_size=20,
    max_overflow=10,
    pool_pre_ping=True,  # Vérifier connexions
    pool_recycle=3600,   # Recycler après 1h
)

# Query optimization
from sqlalchemy.orm import selectinload, joinedload

# Éviter N+1 queries
users = await session.execute(
    select(User)
    .options(selectinload(User.orders))  # Eager loading
    .where(User.active == True)
)

# Indexes
class User(Base):
    __tablename__ = "users"
    
    id = Column(Integer, primary_key=True, index=True)
    email = Column(String, unique=True, index=True)  # Index
    created_at = Column(DateTime, index=True)  # Index pour tri

# Composite index
Index('idx_user_email_active', User.email, User.active)


# === CACHING STRATEGIES ===

# Multi-level cache
class MultiLevelCache:
    def __init__(self):
        self.l1_cache = {}  # In-memory
        self.l2_cache = RedisCache()  # Redis
    
    async def get(self, key: str):
        # L1: Mémoire
        if key in self.l1_cache:
            return self.l1_cache[key]
        
        # L2: Redis
        value = await self.l2_cache.get(key)
        if value:
            self.l1_cache[key] = value
            return value
        
        return None
    
    async def set(self, key: str, value: Any, ttl: int = 300):
        # Écrire dans les deux
        self.l1_cache[key] = value
        await self.l2_cache.set(key, value, ttl)


# === ASYNC OPTIMIZATION ===

# Batch operations
async def batch_get_users(user_ids: List[int]) -> List[User]:
    """Récupérer plusieurs utilisateurs en une requête"""
    return await session.execute(
        select(User).where(User.id.in_(user_ids))
    )

# Concurrent requests
import asyncio

async def get_user_with_orders(user_id: int):
    # Exécuter en parallèle
    user_task = db.get_user(user_id)
    orders_task = orders_service.get_user_orders(user_id)
    
    user, orders = await asyncio.gather(user_task, orders_task)
    
    return {"user": user, "orders": orders}


# === COMPRESSION ===
from fastapi.middleware.gzip import GZIPMiddleware

app.add_middleware(GZIPMiddleware, minimum_size=1000)


# === HTTP/2 ===
# uvicorn avec HTTP/2
uvicorn main:app --http h2


[OK] EXAMPLES COMPLETS

# === EXEMPLE 1: E-COMMERCE MICROSERVICES ===

# Architecture:
# - API Gateway
# - Users Service
# - Products Service
# - Orders Service
# - Inventory Service
# - Payment Service
# - Notification Service

# orders-service/main.py
from fastapi import FastAPI, HTTPException, BackgroundTasks
from pydantic import BaseModel
from typing import List
import httpx
from celery_app import send_notification

app = FastAPI(title="Orders Service")

class OrderItem(BaseModel):
    product_id: int
    quantity: int
    price: float

class OrderCreate(BaseModel):
    user_id: int
    items: List[OrderItem]

class Order(OrderCreate):
    id: int
    status: str
    total: float

orders_db = {}

async def verify_user(user_id: int) -> bool:
    """Vérifier utilisateur existe"""
    async with httpx.AsyncClient() as client:
        try:
            response = await client.get(
                f"http://users-service:8001/users/{user_id}",
                timeout=5.0
            )
            return response.status_code == 200
        except:
            return False

async def check_inventory(items: List[OrderItem]) -> bool:
    """Vérifier stock disponible"""
    async with httpx.AsyncClient() as client:
        for item in items:
            try:
                response = await client.post(
                    "http://inventory-service:8003/check",
                    json={"product_id": item.product_id, "quantity": item.quantity},
                    timeout=5.0
                )
                if response.status_code != 200:
                    return False
            except:
                return False
    return True

async def reserve_inventory(items: List[OrderItem]) -> bool:
    """Réserver stock"""
    async with httpx.AsyncClient() as client:
        for item in items:
            try:
                await client.post(
                    "http://inventory-service:8003/reserve",
                    json={"product_id": item.product_id, "quantity": item.quantity},
                    timeout=5.0
                )
            except:
                return False
    return True

async def process_payment(user_id: int, amount: float) -> bool:
    """Traiter paiement"""
    async with httpx.AsyncClient() as client:
        try:
            response = await client.post(
                "http://payment-service:8004/charge",
                json={"user_id": user_id, "amount": amount},
                timeout=10.0
            )
            return response.status_code == 200
        except:
            return False

@app.post("/orders", response_model=Order, status_code=201)
async def create_order(
    order: OrderCreate,
    background_tasks: BackgroundTasks
):
    """Créer commande avec saga pattern"""
    
    # Étape 1: Vérifier utilisateur
    if not await verify_user(order.user_id):
        raise HTTPException(status_code=404, detail="User not found")
    
    # Étape 2: Vérifier stock
    if not await check_inventory(order.items):
        raise HTTPException(status_code=400, detail="Insufficient inventory")
    
    # Étape 3: Réserver stock
    if not await reserve_inventory(order.items):
        raise HTTPException(status_code=500, detail="Failed to reserve inventory")
    
    # Étape 4: Calculer total
    total = sum(item.price * item.quantity for item in order.items)
    
    # Étape 5: Traiter paiement
    if not await process_payment(order.user_id, total):
        # Annuler réservation
        await release_inventory(order.items)
        raise HTTPException(status_code=402, detail="Payment failed")
    
    # Étape 6: Créer commande
    order_id = len(orders_db) + 1
    new_order = Order(
        id=order_id,
        user_id=order.user_id,
        items=order.items,
        status="confirmed",
        total=total
    )
    orders_db[order_id] = new_order
    
    # Étape 7: Envoyer notification (async)
    background_tasks.add_task(
        send_notification.delay,
        order.user_id,
        f"Order #{order_id} confirmed"
    )
    
    return new_order

@app.get("/orders/{order_id}")
async def get_order(order_id: int):
    if order_id not in orders_db:
        raise HTTPException(status_code=404, detail="Order not found")
    return orders_db[order_id]


# === EXEMPLE 2: REAL-TIME CHAT SERVICE ===

# WebSocket + Redis Pub/Sub
pip install websockets

# chat-service/main.py
from fastapi import FastAPI, WebSocket, WebSocketDisconnect
from typing import List, Dict
import redis.asyncio as redis
import json

app = FastAPI(title="Chat Service")

class ConnectionManager:
    def __init__(self):
        self.active_connections: Dict[str, List[WebSocket]] = {}
        self.redis = None
    
    async def connect(self, websocket: WebSocket, room_id: str):
        await websocket.accept()
        if room_id not in self.active_connections:
            self.active_connections[room_id] = []
        self.active_connections[room_id].append(websocket)
    
    def disconnect(self, websocket: WebSocket, room_id: str):
        self.active_connections[room_id].remove(websocket)
    
    async def broadcast(self, message: dict, room_id: str):
        """Broadcast dans la room"""
        # Publier sur Redis pour autres instances
        await self.redis.publish(
            f"chat:room:{room_id}",
            json.dumps(message)
        )
        
        # Envoyer aux connexions locales
        if room_id in self.active_connections:
            for connection in self.active_connections[room_id]:
                await connection.send_json(message)
    
    async def subscribe_redis(self):
        """S'abonner aux messages Redis"""
        pubsub = self.redis.pubsub()
        await pubsub.psubscribe("chat:room:*")
        
        async for message in pubsub.listen():
            if message["type"] == "pmessage":
                room_id = message["channel"].decode().split(":")[-1]
                data = json.loads(message["data"])
                
                # Envoyer aux connexions locales
                if room_id in self.active_connections:
                    for connection in self.active_connections[room_id]:
                        await connection.send_json(data)

manager = ConnectionManager()

@app.on_event("startup")
async def startup():
    manager.redis = await redis.from_url("redis://localhost:6379")
    # Démarrer subscriber en background
    asyncio.create_task(manager.subscribe_redis())

@app.websocket("/ws/{room_id}")
async def websocket_endpoint(websocket: WebSocket, room_id: str):
    await manager.connect(websocket, room_id)
    try:
        while True:
            data = await websocket.receive_json()
            
            message = {
                "user_id": data["user_id"],
                "message": data["message"],
                "timestamp": datetime.utcnow().isoformat()
            }
            
            await manager.broadcast(message, room_id)
    
    except WebSocketDisconnect:
        manager.disconnect(websocket, room_id)


[OK] RESSOURCES & DOCUMENTATION

# Documentation officielle:
# FastAPI: https://fastapi.tiangolo.com/
# gRPC: https://grpc.io/docs/languages/python/
# Docker: https://docs.docker.com/
# Kubernetes: https://kubernetes.io/docs/
# Istio: https://istio.io/latest/docs/

# Livres recommandés:
# - "Building Microservices" - Sam Newman
# - "Microservices Patterns" - Chris Richardson
# - "Production-Ready Microservices" - Susan J. Fowler
# - "Designing Data-Intensive Applications" - Martin Kleppmann

# Cours en ligne:
# - Udemy: Microservices with Python, Flask, and Docker
# - Coursera: Cloud Architecture with Google Cloud
# - Pluralsight: Microservices Architecture

# Outils de monitoring:
# - Grafana: https://grafana.com/
# - Prometheus: https://prometheus.io/
# - Jaeger: https://www.jaegertracing.io/
# - ELK Stack: https://www.elastic.co/

# Service Mesh:
# - Istio: https://istio.io/
# - Linkerd: https://linkerd.io/
# - Consul: https://www.consul.io/

# Message Brokers:
# - RabbitMQ: https://www.rabbitmq.com/
# - Apache Kafka: https://kafka.apache.org/
# - NATS: https://nats.io/
# - Redis Streams: https://redis.io/topics/streams-intro

# API Gateway:
# - Kong: https://konghq.com/
# - Traefik: https://traefik.io/
# - Ambassador: https://www.getambassador.io/

# Patterns & Best Practices:
# - microservices.io: https://microservices.io/
# - 12factor.net: https://12factor.net/
# - AWS Architecture Center: https://aws.amazon.com/architecture/

# Communautés:
# - Reddit: r/microservices
# - Stack Overflow
# - GitHub Discussions
# - Discord servers (Python, FastAPI)

# ============================================================================
# [OBJECTIF] EXEMPLE COMPLET: E-COMMERCE AVEC FLASK
# ============================================================================

# APPLICATION COMPLÈTE: Plateforme E-commerce avec microservices
# 
# ARCHITECTURE:
# - API Gateway (Flask)
# - Users Service (Flask)
# - Products Service (Flask)
# - Orders Service (Flask)
# - Notifications Service (Celery worker)
# - PostgreSQL (Users, Orders)
# - MongoDB (Products)
# - Redis (Cache, Celery)
# - RabbitMQ (Messages)

# ============================================================================
# STRUCTURE DU PROJET
# ============================================================================

"""
ecommerce-microservices/
├── docker-compose.yml
├── .env
├── requirements.txt
│
├── gateway/
│   ├── app.py
│   ├── auth.py
│   ├── rate_limiter.py
│   ├── circuit_breaker.py
│   ├── config.py
│   └── Dockerfile
│
├── users-service/
│   ├── app.py
│   ├── models.py
│   ├── database.py
│   ├── auth.py
│   ├── config.py
│   ├── requirements.txt
│   └── Dockerfile
│
├── products-service/
│   ├── app.py
│   ├── models.py
│   ├── database.py
│   ├── config.py
│   ├── requirements.txt
│   └── Dockerfile
│
├── orders-service/
│   ├── app.py
│   ├── models.py
│   ├── database.py
│   ├── saga.py
│   ├── config.py
│   ├── requirements.txt
│   └── Dockerfile
│
├── notifications-service/
│   ├── worker.py
│   ├── tasks.py
│   ├── config.py
│   ├── requirements.txt
│   └── Dockerfile
│
└── shared/
    ├── __init__.py
    ├── logging_config.py
    └── monitoring.py
"""

# ============================================================================
# 1. CONFIGURATION GLOBALE
# ============================================================================

# Fichier: .env
"""
# Database URLs
USERS_DATABASE_URL=postgresql://admin:secret@postgres:5432/users_db
ORDERS_DATABASE_URL=postgresql://admin:secret@postgres:5432/orders_db
PRODUCTS_DATABASE_URL=mongodb://mongo:27017/products_db

# Redis
REDIS_URL=redis://redis:6379/0

# RabbitMQ
RABBITMQ_URL=amqp://admin:admin@rabbitmq:5672/

# JWT
JWT_SECRET_KEY=your-super-secret-jwt-key-change-in-production-min-32-chars

# Service URLs
USERS_SERVICE_URL=http://users-service:5001
PRODUCTS_SERVICE_URL=http://products-service:5002
ORDERS_SERVICE_URL=http://orders-service:5003
"""

# Fichier: docker-compose.yml
"""
version: '3.8'

services:
  # ========== Infrastructure ==========
  
  postgres:
    image: postgres:15
    container_name: ecommerce-postgres
    environment:
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
      POSTGRES_MULTIPLE_DATABASES: users_db,orders_db
    ports:
      - "5432:5432"
    volumes:
      - postgres_data:/var/lib/postgresql/data
    networks:
      - ecommerce-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U admin"]
      interval: 10s
      timeout: 5s
      retries: 5
  
  mongo:
    image: mongo:7
    container_name: ecommerce-mongo
    ports:
      - "27017:27017"
    volumes:
      - mongo_data:/data/db
    networks:
      - ecommerce-network
  
  redis:
    image: redis:7-alpine
    container_name: ecommerce-redis
    ports:
      - "6379:6379"
    networks:
      - ecommerce-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
  
  rabbitmq:
    image: rabbitmq:3-management
    container_name: ecommerce-rabbitmq
    environment:
      RABBITMQ_DEFAULT_USER: admin
      RABBITMQ_DEFAULT_PASS: admin
    ports:
      - "5672:5672"
      - "15672:15672"
    networks:
      - ecommerce-network
    healthcheck:
      test: ["CMD", "rabbitmq-diagnostics", "ping"]
      interval: 30s
  
  # ========== Services ==========
  
  gateway:
    build: ./gateway
    container_name: api-gateway
    env_file: .env
    ports:
      - "5000:5000"
    depends_on:
      - redis
      - users-service
      - products-service
      - orders-service
    networks:
      - ecommerce-network
    restart: unless-stopped
  
  users-service:
    build: ./users-service
    container_name: users-service
    env_file: .env
    expose:
      - "5001"
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - ecommerce-network
    restart: unless-stopped
  
  products-service:
    build: ./products-service
    container_name: products-service
    env_file: .env
    expose:
      - "5002"
    depends_on:
      - mongo
      - redis
    networks:
      - ecommerce-network
    restart: unless-stopped
  
  orders-service:
    build: ./orders-service
    container_name: orders-service
    env_file: .env
    expose:
      - "5003"
    depends_on:
      postgres:
        condition: service_healthy
      rabbitmq:
        condition: service_healthy
    networks:
      - ecommerce-network
    restart: unless-stopped
  
  notifications-worker:
    build: ./notifications-service
    container_name: notifications-worker
    env_file: .env
    depends_on:
      rabbitmq:
        condition: service_healthy
    networks:
      - ecommerce-network
    restart: unless-stopped
    command: celery -A tasks worker --loglevel=info

networks:
  ecommerce-network:
    driver: bridge

volumes:
  postgres_data:
  mongo_data:
"""

# ============================================================================
# 2. SHARED UTILITIES
# ============================================================================

# Fichier: shared/logging_config.py
"""
import logging
import sys
from pythonjsonlogger import jsonlogger

def setup_logging(service_name: str):
    '''Configure structured logging for service'''
    
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    
    # JSON formatter
    logHandler = logging.StreamHandler(sys.stdout)
    formatter = jsonlogger.JsonFormatter(
        fmt='%(asctime)s %(name)s %(levelname)s %(message)s'
    )
    logHandler.setFormatter(formatter)
    logger.addHandler(logHandler)
    
    return logger
"""

# Fichier: shared/monitoring.py
"""
from prometheus_client import Counter, Histogram, generate_latest
from functools import wraps
import time

# Métriques communes
request_count = Counter(
    'http_requests_total',
    'Total HTTP requests',
    ['service', 'method', 'endpoint', 'status']
)

request_duration = Histogram(
    'http_request_duration_seconds',
    'HTTP request duration',
    ['service', 'method', 'endpoint']
)

def track_metrics(service_name):
    '''Décorateur pour tracker métriques'''
    def decorator(f):
        @wraps(f)
        def wrapper(*args, **kwargs):
            start_time = time.time()
            
            try:
                result = f(*args, **kwargs)
                status = getattr(result, 'status_code', 200)
                
                request_count.labels(
                    service=service_name,
                    method=request.method,
                    endpoint=request.endpoint,
                    status=status
                ).inc()
                
                return result
            finally:
                duration = time.time() - start_time
                request_duration.labels(
                    service=service_name,
                    method=request.method,
                    endpoint=request.endpoint
                ).observe(duration)
        
        return wrapper
    return decorator
"""

# ============================================================================
# 3. USERS SERVICE (Flask)
# ============================================================================

# Fichier: users-service/config.py
"""
import os

class Config:
    DATABASE_URL = os.getenv('USERS_DATABASE_URL')
    REDIS_URL = os.getenv('REDIS_URL')
    JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY')
    JWT_ALGORITHM = 'HS256'
    JWT_EXPIRE_MINUTES = 30
"""

# Fichier: users-service/database.py
"""
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate

db = SQLAlchemy()
migrate = Migrate()

def init_db(app):
    db.init_app(app)
    migrate.init_app(app, db)
    
    with app.app_context():
        db.create_all()
"""

# Fichier: users-service/models.py
"""
from database import db
from datetime import datetime
from werkzeug.security import generate_password_hash, check_password_hash

class User(db.Model):
    __tablename__ = 'users'
    
    id = db.Column(db.Integer, primary_key=True)
    email = db.Column(db.String(255), unique=True, nullable=False, index=True)
    name = db.Column(db.String(255), nullable=False)
    password_hash = db.Column(db.String(255), nullable=False)
    active = db.Column(db.Boolean, default=True)
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    def set_password(self, password):
        self.password_hash = generate_password_hash(password)
    
    def check_password(self, password):
        return check_password_hash(self.password_hash, password)
    
    def to_dict(self):
        return {
            'id': self.id,
            'email': self.email,
            'name': self.name,
            'active': self.active,
            'created_at': self.created_at.isoformat()
        }
"""

# Fichier: users-service/auth.py
"""
from datetime import datetime, timedelta
import jwt
from flask import jsonify
from functools import wraps
from flask import request
from config import Config

def create_access_token(user_id, email, role='user'):
    '''Créer JWT token'''
    payload = {
        'sub': str(user_id),
        'email': email,
        'role': role,
        'exp': datetime.utcnow() + timedelta(minutes=Config.JWT_EXPIRE_MINUTES)
    }
    
    token = jwt.encode(payload, Config.JWT_SECRET_KEY, algorithm=Config.JWT_ALGORITHM)
    return token

def decode_token(token):
    '''Décoder JWT token'''
    try:
        payload = jwt.decode(token, Config.JWT_SECRET_KEY, algorithms=[Config.JWT_ALGORITHM])
        return payload
    except jwt.ExpiredSignatureError:
        return None
    except jwt.InvalidTokenError:
        return None

def token_required(f):
    '''Décorateur pour routes protégées'''
    @wraps(f)
    def decorated(*args, **kwargs):
        # Extraire token du header
        auth_header = request.headers.get('Authorization')
        
        if not auth_header or not auth_header.startswith('Bearer '):
            return jsonify({'error': 'Missing or invalid token'}), 401
        
        token = auth_header.split(' ')[1]
        payload = decode_token(token)
        
        if not payload:
            return jsonify({'error': 'Invalid or expired token'}), 401
        
        # Ajouter user info à request
        request.user = payload
        
        return f(*args, **kwargs)
    
    return decorated
"""

# Fichier: users-service/app.py
"""
from flask import Flask, request, jsonify
from flask_cors import CORS
from config import Config
from database import db, init_db
from models import User
from auth import create_access_token, token_required
import redis
import json
from prometheus_client import generate_latest
import logging

# Créer app Flask
app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = Config.DATABASE_URL
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

# Extensions
CORS(app)
init_db(app)

# Redis pour cache
redis_client = redis.from_url(Config.REDIS_URL)

# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# ========== ENDPOINTS ==========

@app.route('/health', methods=['GET'])
def health():
    '''Health check'''
    return jsonify({'status': 'healthy', 'service': 'users'}), 200

@app.route('/metrics', methods=['GET'])
def metrics():
    '''Prometheus metrics'''
    return generate_latest()

@app.route('/api/register', methods=['POST'])
def register():
    '''Inscription utilisateur'''
    data = request.get_json()
    
    # Validation
    if not data or not data.get('email') or not data.get('password'):
        return jsonify({'error': 'Email and password required'}), 400
    
    # Vérifier si email existe
    if User.query.filter_by(email=data['email']).first():
        return jsonify({'error': 'Email already registered'}), 400
    
    try:
        # Créer utilisateur
        user = User(
            email=data['email'],
            name=data.get('name', '')
        )
        user.set_password(data['password'])
        
        db.session.add(user)
        db.session.commit()
        
        logger.info(f"User registered: {user.email}")
        
        return jsonify(user.to_dict()), 201
    
    except Exception as e:
        db.session.rollback()
        logger.error(f"Registration error: {str(e)}")
        return jsonify({'error': 'Registration failed'}), 500

@app.route('/api/login', methods=['POST'])
def login():
    '''Connexion utilisateur'''
    data = request.get_json()
    
    if not data or not data.get('email') or not data.get('password'):
        return jsonify({'error': 'Email and password required'}), 400
    
    # Trouver utilisateur
    user = User.query.filter_by(email=data['email']).first()
    
    if not user or not user.check_password(data['password']):
        return jsonify({'error': 'Invalid credentials'}), 401
    
    if not user.active:
        return jsonify({'error': 'Account disabled'}), 403
    
    # Créer token
    token = create_access_token(user.id, user.email)
    
    logger.info(f"User logged in: {user.email}")
    
    return jsonify({
        'access_token': token,
        'token_type': 'bearer',
        'user': user.to_dict()
    }), 200

@app.route('/api/users/me', methods=['GET'])
@token_required
def get_current_user():
    '''Récupérer utilisateur connecté'''
    user_id = int(request.user['sub'])
    
    # Vérifier cache
    cache_key = f"user:{user_id}"
    cached = redis_client.get(cache_key)
    
    if cached:
        return jsonify(json.loads(cached)), 200
    
    # Récupérer de la DB
    user = User.query.get(user_id)
    
    if not user:
        return jsonify({'error': 'User not found'}), 404
    
    user_dict = user.to_dict()
    
    # Mettre en cache (5 minutes)
    redis_client.setex(cache_key, 300, json.dumps(user_dict))
    
    return jsonify(user_dict), 200

@app.route('/api/users/<int:user_id>', methods=['GET'])
def get_user(user_id):
    '''Récupérer utilisateur par ID (pour autres services)'''
    # Vérifier cache
    cache_key = f"user:{user_id}"
    cached = redis_client.get(cache_key)
    
    if cached:
        return jsonify(json.loads(cached)), 200
    
    user = User.query.get(user_id)
    
    if not user:
        return jsonify({'error': 'User not found'}), 404
    
    user_dict = user.to_dict()
    redis_client.setex(cache_key, 300, json.dumps(user_dict))
    
    return jsonify(user_dict), 200

@app.route('/api/users', methods=['GET'])
@token_required
def list_users():
    '''Lister utilisateurs (admin only)'''
    if request.user.get('role') != 'admin':
        return jsonify({'error': 'Admin access required'}), 403
    
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 10, type=int)
    
    pagination = User.query.paginate(page=page, per_page=per_page, error_out=False)
    
    return jsonify({
        'users': [u.to_dict() for u in pagination.items],
        'total': pagination.total,
        'pages': pagination.pages,
        'current_page': page
    }), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5001, debug=False)
"""

# Fichier: users-service/requirements.txt
"""
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
Flask-Migrate==4.0.5
Flask-CORS==4.0.0
psycopg2-binary==2.9.9
PyJWT==2.8.0
redis==5.0.1
Werkzeug==3.0.1
prometheus-client==0.19.0
python-json-logger==2.0.7
"""

# Fichier: users-service/Dockerfile
"""
FROM python:3.11-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 5001

HEALTHCHECK --interval=30s --timeout=3s \
  CMD python -c "import requests; requests.get('http://localhost:5001/health')"

CMD ["python", "app.py"]
"""

# ============================================================================
# 4. PRODUCTS SERVICE (Flask + MongoDB)
# ============================================================================

# Fichier: products-service/app.py
"""
from flask import Flask, request, jsonify
from flask_cors import CORS
from pymongo import MongoClient
from bson import ObjectId
import os
import redis
import json
import logging

app = Flask(__name__)
CORS(app)

# MongoDB
mongo_client = MongoClient(os.getenv('PRODUCTS_DATABASE_URL'))
db = mongo_client.products_db
products_collection = db.products

# Redis
redis_client = redis.from_url(os.getenv('REDIS_URL'))

# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# ========== ENDPOINTS ==========

@app.route('/health', methods=['GET'])
def health():
    return jsonify({'status': 'healthy', 'service': 'products'}), 200

@app.route('/api/products', methods=['GET'])
def list_products():
    '''Lister produits avec filtres'''
    # Paramètres
    category = request.args.get('category')
    min_price = request.args.get('min_price', type=float)
    max_price = request.args.get('max_price', type=float)
    page = request.args.get('page', 1, type=int)
    per_page = request.args.get('per_page', 20, type=int)
    
    # Construire query
    query = {}
    if category:
        query['category'] = category
    if min_price or max_price:
        query['price'] = {}
        if min_price:
            query['price']['$gte'] = min_price
        if max_price:
            query['price']['$lte'] = max_price
    
    # Pagination
    skip = (page - 1) * per_page
    
    # Exécuter query
    cursor = products_collection.find(query).skip(skip).limit(per_page)
    products = []
    
    for doc in cursor:
        doc['_id'] = str(doc['_id'])
        products.append(doc)
    
    total = products_collection.count_documents(query)
    
    return jsonify({
        'products': products,
        'total': total,
        'page': page,
        'per_page': per_page
    }), 200

@app.route('/api/products/<product_id>', methods=['GET'])
def get_product(product_id):
    '''Récupérer produit par ID'''
    # Vérifier cache
    cache_key = f"product:{product_id}"
    cached = redis_client.get(cache_key)
    
    if cached:
        return jsonify(json.loads(cached)), 200
    
    # Récupérer de MongoDB
    try:
        product = products_collection.find_one({'_id': ObjectId(product_id)})
        
        if not product:
            return jsonify({'error': 'Product not found'}), 404
        
        product['_id'] = str(product['_id'])
        
        # Cache 10 minutes
        redis_client.setex(cache_key, 600, json.dumps(product))
        
        return jsonify(product), 200
    
    except Exception as e:
        logger.error(f"Error fetching product: {str(e)}")
        return jsonify({'error': 'Invalid product ID'}), 400

@app.route('/api/products', methods=['POST'])
def create_product():
    '''Créer produit (admin only)'''
    data = request.get_json()
    
    # Validation
    required_fields = ['name', 'price', 'category', 'stock']
    if not all(field in data for field in required_fields):
        return jsonify({'error': 'Missing required fields'}), 400
    
    try:
        # Insérer dans MongoDB
        result = products_collection.insert_one(data)
        
        # Récupérer produit créé
        product = products_collection.find_one({'_id': result.inserted_id})
        product['_id'] = str(product['_id'])
        
        logger.info(f"Product created: {product['name']}")
        
        return jsonify(product), 201
    
    except Exception as e:
        logger.error(f"Error creating product: {str(e)}")
        return jsonify({'error': 'Failed to create product'}), 500

@app.route('/api/products/<product_id>/stock', methods=['PUT'])
def update_stock(product_id):
    '''Mettre à jour stock (pour orders service)'''
    data = request.get_json()
    
    if 'quantity' not in data:
        return jsonify({'error': 'Quantity required'}), 400
    
    try:
        result = products_collection.update_one(
            {'_id': ObjectId(product_id)},
            {'$inc': {'stock': data['quantity']}}
        )
        
        if result.matched_count == 0:
            return jsonify({'error': 'Product not found'}), 404
        
        # Invalider cache
        redis_client.delete(f"product:{product_id}")
        
        logger.info(f"Stock updated for product {product_id}")
        
        return jsonify({'message': 'Stock updated'}), 200
    
    except Exception as e:
        logger.error(f"Error updating stock: {str(e)}")
        return jsonify({'error': 'Failed to update stock'}), 500

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5002, debug=False)
"""

# Fichier: products-service/requirements.txt
"""
Flask==3.0.0
Flask-CORS==4.0.0
pymongo==4.6.1
redis==5.0.1
prometheus-client==0.19.0
"""

# ============================================================================
# 5. ORDERS SERVICE (Flask + SAGA Pattern)
# ============================================================================

# Fichier: orders-service/app.py
"""
from flask import Flask, request, jsonify
from flask_cors import CORS
from database import db, init_db
from models import Order, OrderItem
from saga import OrderSaga
import requests
import logging
from celery_app import send_notification

app = Flask(__name__)
app.config['SQLALCHEMY_DATABASE_URI'] = os.getenv('ORDERS_DATABASE_URL')
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False

CORS(app)
init_db(app)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# URLs des services
USERS_SERVICE = os.getenv('USERS_SERVICE_URL')
PRODUCTS_SERVICE = os.getenv('PRODUCTS_SERVICE_URL')

@app.route('/health', methods=['GET'])
def health():
    return jsonify({'status': 'healthy', 'service': 'orders'}), 200

@app.route('/api/orders', methods=['POST'])
def create_order():
    '''Créer commande avec SAGA pattern'''
    data = request.get_json()
    
    # Extraire user_id du header (ajouté par gateway)
    user_id = request.headers.get('X-User-ID')
    
    if not user_id:
        return jsonify({'error': 'User not authenticated'}), 401
    
    if not data or not data.get('items'):
        return jsonify({'error': 'Items required'}), 400
    
    try:
        # Exécuter SAGA
        saga = OrderSaga(user_id, data['items'])
        result = saga.execute()
        
        if result['success']:
            order = result['order']
            
            # Envoyer notification asynchrone
            send_notification.delay(
                user_id=int(user_id),
                message=f"Order #{order.id} created successfully!",
                type='order_created'
            )
            
            return jsonify(order.to_dict()), 201
        else:
            return jsonify({'error': result['error']}), 400
    
    except Exception as e:
        logger.error(f"Order creation error: {str(e)}")
        return jsonify({'error': 'Failed to create order'}), 500

@app.route('/api/orders/<int:order_id>', methods=['GET'])
def get_order(order_id):
    '''Récupérer commande'''
    user_id = request.headers.get('X-User-ID')
    
    order = Order.query.get(order_id)
    
    if not order:
        return jsonify({'error': 'Order not found'}), 404
    
    # Vérifier que l'order appartient à l'utilisateur
    if str(order.user_id) != user_id:
        return jsonify({'error': 'Unauthorized'}), 403
    
    return jsonify(order.to_dict()), 200

@app.route('/api/users/<int:user_id>/orders', methods=['GET'])
def list_user_orders(user_id):
    '''Lister commandes d'un utilisateur'''
    requesting_user_id = request.headers.get('X-User-ID')
    
    # Vérifier autorisation
    if str(user_id) != requesting_user_id:
        return jsonify({'error': 'Unauthorized'}), 403
    
    orders = Order.query.filter_by(user_id=user_id).order_by(Order.created_at.desc()).all()
    
    return jsonify({
        'orders': [o.to_dict() for o in orders]
    }), 200

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5003, debug=False)
"""

# Fichier: orders-service/saga.py
"""
import requests
import logging
from models import Order, OrderItem
from database import db

logger = logging.getLogger(__name__)

class OrderSaga:
    '''SAGA pattern pour création de commande'''
    
    def __init__(self, user_id, items):
        self.user_id = user_id
        self.items = items
        self.order = None
        self.reserved_products = []
    
    def execute(self):
        '''Exécuter SAGA'''
        try:
            # Étape 1: Vérifier utilisateur existe
            if not self._verify_user():
                return {'success': False, 'error': 'User not found'}
            
            # Étape 2: Vérifier et réserver stock
            if not self._reserve_stock():
                return {'success': False, 'error': 'Insufficient stock'}
            
            # Étape 3: Calculer total
            total = self._calculate_total()
            
            # Étape 4: Créer commande
            if not self._create_order(total):
                self._compensate()
                return {'success': False, 'error': 'Failed to create order'}
            
            return {'success': True, 'order': self.order}
        
        except Exception as e:
            logger.error(f"SAGA error: {str(e)}")
            self._compensate()
            return {'success': False, 'error': str(e)}
    
    def _verify_user(self):
        '''Vérifier que l'utilisateur existe'''
        try:
            response = requests.get(
                f"{USERS_SERVICE}/api/users/{self.user_id}",
                timeout=5
            )
            return response.status_code == 200
        except:
            return False
    
    def _reserve_stock(self):
        '''Réserver stock pour chaque produit'''
        for item in self.items:
            try:
                # Vérifier stock disponible
                response = requests.get(
                    f"{PRODUCTS_SERVICE}/api/products/{item['product_id']}",
                    timeout=5
                )
                
                if response.status_code != 200:
                    return False
                
                product = response.json()
                
                if product['stock'] < item['quantity']:
                    return False
                
                # Réserver (décrémenter stock)
                response = requests.put(
                    f"{PRODUCTS_SERVICE}/api/products/{item['product_id']}/stock",
                    json={'quantity': -item['quantity']},
                                        timeout=5
                )
                
                if response.status_code == 200:
                    self.reserved_products.append({
                        'product_id': item['product_id'],
                        'quantity': item['quantity'],
                        'price': product['price']
                    })
                else:
                    return False
            
            except Exception as e:
                logger.error(f"Stock reservation error: {str(e)}")
                return False
        
        return True
    
    def _calculate_total(self):
        '''Calculer total de la commande'''
        total = sum(p['price'] * p['quantity'] for p in self.reserved_products)
        return total
    
    def _create_order(self, total):
        '''Créer commande en base de données'''
        try:
            # Créer order
            order = Order(
                user_id=self.user_id,
                total=total,
                status='created'
            )
            db.session.add(order)
            db.session.flush()  # Get order ID
            
            # Créer order items
            for product in self.reserved_products:
                item = OrderItem(
                    order_id=order.id,
                    product_id=product['product_id'],
                    quantity=product['quantity'],
                    price=product['price']
                )
                db.session.add(item)
            
            db.session.commit()
            self.order = order
            
            logger.info(f"Order created: {order.id}")
            return True
        
        except Exception as e:
            db.session.rollback()
            logger.error(f"Order creation error: {str(e)}")
            return False
    
    def _compensate(self):
        '''Annuler réservations en cas d'échec (compensation)'''
        logger.info("Compensating SAGA - releasing reserved stock")
        
        for product in self.reserved_products:
            try:
                # Remettre le stock
                requests.put(
                    f"{PRODUCTS_SERVICE}/api/products/{product['product_id']}/stock",
                    json={'quantity': product['quantity']},
                    timeout=5
                )
            except Exception as e:
                logger.error(f"Compensation error: {str(e)}")
"""

# Fichier: orders-service/models.py
"""
from database import db
from datetime import datetime

class Order(db.Model):
    __tablename__ = 'orders'
    
    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, nullable=False, index=True)
    total = db.Column(db.Float, nullable=False)
    status = db.Column(db.String(50), default='created')
    created_at = db.Column(db.DateTime, default=datetime.utcnow)
    
    # Relation avec OrderItem
    items = db.relationship('OrderItem', backref='order', lazy=True, cascade='all, delete-orphan')
    
    def to_dict(self):
        return {
            'id': self.id,
            'user_id': self.user_id,
            'total': self.total,
            'status': self.status,
            'created_at': self.created_at.isoformat(),
            'items': [item.to_dict() for item in self.items]
        }

class OrderItem(db.Model):
    __tablename__ = 'order_items'
    
    id = db.Column(db.Integer, primary_key=True)
    order_id = db.Column(db.Integer, db.ForeignKey('orders.id'), nullable=False)
    product_id = db.Column(db.String(50), nullable=False)
    quantity = db.Column(db.Integer, nullable=False)
    price = db.Column(db.Float, nullable=False)
    
    def to_dict(self):
        return {
            'id': self.id,
            'product_id': self.product_id,
            'quantity': self.quantity,
            'price': self.price
        }
"""

# Fichier: orders-service/celery_app.py
"""
from celery import Celery
import os

celery_app = Celery(
    'orders',
    broker=os.getenv('RABBITMQ_URL'),
    backend=os.getenv('REDIS_URL')
)

celery_app.conf.update(
    task_serializer='json',
    accept_content=['json'],
    result_serializer='json',
)

@celery_app.task
def send_notification(user_id, message, type):
    '''Envoyer notification (traité par notifications-service)'''
    # Publier événement
    celery_app.send_task(
        'notifications.send',
        args=[user_id, message, type]
    )
"""

# Fichier: orders-service/requirements.txt
"""
Flask==3.0.0
Flask-SQLAlchemy==3.1.1
Flask-Migrate==4.0.5
Flask-CORS==4.0.0
psycopg2-binary==2.9.9
requests==2.31.0
celery==5.3.4
redis==5.0.1
"""

# ============================================================================
# 6. NOTIFICATIONS SERVICE (Celery Worker)
# ============================================================================

# Fichier: notifications-service/tasks.py
"""
from celery import Celery
import os
import logging

# Configuration Celery
celery_app = Celery(
    'notifications',
    broker=os.getenv('RABBITMQ_URL'),
    backend=os.getenv('REDIS_URL')
)

celery_app.conf.update(
    task_serializer='json',
    accept_content=['json'],
    result_serializer='json',
)

logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

@celery_app.task(name='notifications.send', bind=True, max_retries=3)
def send_notification(self, user_id, message, type):
    '''
    Envoyer notification
    
    En production: SendGrid, AWS SES, Twilio, etc.
    Ici: Simulation
    '''
    try:
        logger.info(f"Sending {type} notification to user {user_id}: {message}")
        
        # Simuler envoi email/SMS/push
        # email_service.send(user_id, message)
        
        logger.info(f"Notification sent successfully to user {user_id}")
        
        return {'status': 'sent', 'user_id': user_id}
    
    except Exception as e:
        logger.error(f"Notification error: {str(e)}")
        
        # Retry avec exponential backoff
        raise self.retry(exc=e, countdown=60 * (2 ** self.request.retries))

@celery_app.task(name='notifications.order_created')
def notify_order_created(order_id, user_id, total):
    '''Notification spécifique pour commande créée'''
    message = f"Your order #{order_id} for ${total:.2f} has been created!"
    send_notification.delay(user_id, message, 'order_created')

@celery_app.task(name='notifications.welcome')
def send_welcome_email(user_id, email):
    '''Email de bienvenue'''
    message = f"Welcome to our e-commerce platform! Your account ({email}) is ready."
    send_notification.delay(user_id, message, 'welcome')
"""

# Fichier: notifications-service/requirements.txt
"""
celery==5.3.4
redis==5.0.1
"""

# ============================================================================
# 7. API GATEWAY (Flask)
# ============================================================================

# Fichier: gateway/config.py
"""
import os

class Config:
    # Services backend
    SERVICES = {
        '/api/register': {'url': os.getenv('USERS_SERVICE_URL'), 'auth': False},
        '/api/login': {'url': os.getenv('USERS_SERVICE_URL'), 'auth': False},
        '/api/users': {'url': os.getenv('USERS_SERVICE_URL'), 'auth': True},
        '/api/products': {'url': os.getenv('PRODUCTS_SERVICE_URL'), 'auth': False},
        '/api/orders': {'url': os.getenv('ORDERS_SERVICE_URL'), 'auth': True},
    }
    
    # Redis
    REDIS_URL = os.getenv('REDIS_URL')
    
    # JWT
    JWT_SECRET_KEY = os.getenv('JWT_SECRET_KEY')
    JWT_ALGORITHM = 'HS256'
    
    # Rate limiting
    RATE_LIMIT_PER_MINUTE = 100
    RATE_LIMIT_WINDOW = 60
"""

# Fichier: gateway/auth.py
"""
import jwt
from config import Config
from flask import request, jsonify
from functools import wraps

def decode_token(token):
    '''Décoder JWT token'''
    try:
        payload = jwt.decode(
            token, 
            Config.JWT_SECRET_KEY, 
            algorithms=[Config.JWT_ALGORITHM]
        )
        return payload
    except jwt.ExpiredSignatureError:
        return None
    except jwt.InvalidTokenError:
        return None

def verify_token():
    '''Vérifier token dans header'''
    auth_header = request.headers.get('Authorization')
    
    if not auth_header or not auth_header.startswith('Bearer '):
        return None
    
    token = auth_header.split(' ')[1]
    return decode_token(token)
"""

# Fichier: gateway/rate_limiter.py
"""
from datetime import datetime, timedelta
from collections import defaultdict
import asyncio

class RateLimiter:
    '''Rate limiter en mémoire (production: Redis)'''
    
    def __init__(self, redis_client):
        self.redis = redis_client
    
    def is_allowed(self, key, max_requests=100, window=60):
        '''
        Vérifier si requête autorisée
        
        Returns: (allowed: bool, retry_after: int)
        '''
        pipe = self.redis.pipeline()
        now = datetime.utcnow().timestamp()
        window_start = now - window
        
        # Clé Redis avec timestamp
        redis_key = f"rate_limit:{key}"
        
        # Supprimer vieilles entrées
        pipe.zremrangebyscore(redis_key, 0, window_start)
        
        # Compter requêtes dans la fenêtre
        pipe.zcard(redis_key)
        
        # Ajouter requête actuelle
        pipe.zadd(redis_key, {str(now): now})
        
        # Expiration
        pipe.expire(redis_key, window)
        
        results = pipe.execute()
        
        count = results[1]
        
        if count >= max_requests:
            # Obtenir plus vieille entrée
            oldest = self.redis.zrange(redis_key, 0, 0, withscores=True)
            if oldest:
                retry_after = int(window - (now - oldest[0][1]))
            else:
                retry_after = window
            
            return False, retry_after
        
        return True, 0
"""

# Fichier: gateway/circuit_breaker.py
"""
from datetime import datetime, timedelta
import logging

logger = logging.getLogger(__name__)

class CircuitBreaker:
    '''Circuit breaker pattern'''
    
    def __init__(self, failure_threshold=5, timeout=60, success_threshold=2):
        self.failure_threshold = failure_threshold
        self.timeout = timeout
        self.success_threshold = success_threshold
        
        self.failures = 0
        self.successes = 0
        self.last_failure_time = None
        self.state = 'CLOSED'  # CLOSED, OPEN, HALF_OPEN
    
    def call(self, func, *args, **kwargs):
        '''Exécuter fonction avec circuit breaker'''
        
        # Vérifier état
        if self.state == 'OPEN':
            if datetime.utcnow() - self.last_failure_time > timedelta(seconds=self.timeout):
                self.state = 'HALF_OPEN'
                self.successes = 0
                logger.info("Circuit breaker: OPEN -> HALF_OPEN")
            else:
                raise Exception("Circuit breaker is OPEN")
        
        try:
            result = func(*args, **kwargs)
            self.on_success()
            return result
        except Exception as e:
            self.on_failure()
            raise e
    
    def on_success(self):
        '''Enregistrer succès'''
        self.failures = 0
        
        if self.state == 'HALF_OPEN':
            self.successes += 1
            if self.successes >= self.success_threshold:
                self.state = 'CLOSED'
                logger.info("Circuit breaker: HALF_OPEN -> CLOSED")
    
    def on_failure(self):
        '''Enregistrer échec'''
        self.failures += 1
        self.last_failure_time = datetime.utcnow()
        
        if self.failures >= self.failure_threshold:
            self.state = 'OPEN'
            logger.warning(f"Circuit breaker: CLOSED -> OPEN")
"""

# Fichier: gateway/app.py
"""
from flask import Flask, request, jsonify, Response
from flask_cors import CORS
import requests
import redis
from config import Config
from auth import verify_token
from rate_limiter import RateLimiter
from circuit_breaker import CircuitBreaker
import logging
import time

# Créer app
app = Flask(__name__)
CORS(app)

# Redis
redis_client = redis.from_url(Config.REDIS_URL)

# Rate limiter
rate_limiter = RateLimiter(redis_client)

# Circuit breakers (un par service)
circuit_breakers = {
    service: CircuitBreaker()
    for service in set(cfg['url'] for cfg in Config.SERVICES.values())
}

# Logging
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)

# ========== HELPERS ==========

def get_service_config(path):
    '''Trouver configuration du service pour un path'''
    for prefix, config in Config.SERVICES.items():
        if path.startswith(prefix):
            return prefix, config
    return None, None

def forward_request(service_url, path, method, headers, data, params):
    '''Forward requête au service backend'''
    
    url = f"{service_url}{path}"
    
    # Nettoyer headers
    forward_headers = {k: v for k, v in headers.items() if k.lower() != 'host'}
    
    # Ajouter headers gateway
    forward_headers['X-Forwarded-For'] = request.remote_addr
    forward_headers['X-Gateway-Timestamp'] = str(time.time())
    
    # Forward
    response = requests.request(
        method=method,
        url=url,
        headers=forward_headers,
        data=data,
        params=params,
        timeout=10
    )
    
    return response

# ========== MIDDLEWARES ==========

@app.before_request
def rate_limit_middleware():
    '''Rate limiting par IP'''
    
    # Exclure health check
    if request.path == '/health':
        return None
    
    # Clé: IP du client
    client_ip = request.remote_addr
    
    # Vérifier rate limit
    allowed, retry_after = rate_limiter.is_allowed(
        key=f"ip:{client_ip}",
        max_requests=Config.RATE_LIMIT_PER_MINUTE,
        window=Config.RATE_LIMIT_WINDOW
    )
    
    if not allowed:
        logger.warning(f"Rate limit exceeded for IP: {client_ip}")
        return jsonify({
            'error': 'Rate limit exceeded',
            'retry_after': retry_after
        }), 429

@app.before_request
def auth_middleware():
    '''Vérifier authentification si nécessaire'''
    
    # Routes publiques
    if request.path in ['/health', '/metrics']:
        return None
    
    # Trouver config du service
    prefix, config = get_service_config(request.path)
    
    if not config:
        return None
    
    # Si auth requise
    if config.get('auth', False):
        payload = verify_token()
        
        if not payload:
            logger.warning(f"Unauthorized access attempt to {request.path}")
            return jsonify({'error': 'Unauthorized'}), 401
        
        # Ajouter user info aux headers pour le backend
        request.user_id = payload.get('sub')
        request.user_email = payload.get('email')
        request.user_role = payload.get('role')

# ========== ROUTES ==========

@app.route('/health', methods=['GET'])
def health():
    '''Health check du gateway'''
    return jsonify({'status': 'healthy', 'service': 'gateway'}), 200

@app.route('/health/services', methods=['GET'])
def health_services():
    '''Health check de tous les services backend'''
    results = {}
    
    for prefix, config in Config.SERVICES.items():
        service_url = config['url']
        service_name = service_url.split('//')[1].split(':')[0]
        
        try:
            response = requests.get(f"{service_url}/health", timeout=3)
            
            if response.status_code == 200:
                results[service_name] = {'status': 'healthy'}
            else:
                results[service_name] = {'status': 'unhealthy'}
        except Exception as e:
            results[service_name] = {'status': 'unreachable', 'error': str(e)}
    
    overall_healthy = all(s['status'] == 'healthy' for s in results.values())
    status_code = 200 if overall_healthy else 503
    
    return jsonify({
        'status': 'healthy' if overall_healthy else 'degraded',
        'services': results
    }), status_code

@app.route('/<path:path>', methods=['GET', 'POST', 'PUT', 'DELETE', 'PATCH'])
def gateway_route(path):
    '''Route principale: Forward toutes les requêtes'''
    
    # Reconstruire le path complet
    full_path = f"/{path}"
    
    # Trouver service
    prefix, config = get_service_config(full_path)
    
    if not config:
        logger.warning(f"No service found for path: {full_path}")
        return jsonify({'error': 'Service not found'}), 404
    
    service_url = config['url']
    
    # Préparer headers
    headers = dict(request.headers)
    
    # Ajouter user info si authentifié
    if hasattr(request, 'user_id'):
        headers['X-User-ID'] = str(request.user_id)
        headers['X-User-Email'] = str(request.user_email)
        headers['X-User-Role'] = str(request.user_role)
    
    # Circuit breaker
    breaker = circuit_breakers.get(service_url)
    
    try:
        if breaker:
            # Avec circuit breaker
            response = breaker.call(
                forward_request,
                service_url=service_url,
                path=full_path,
                method=request.method,
                headers=headers,
                data=request.get_data(),
                params=request.args
            )
        else:
            # Sans circuit breaker
            response = forward_request(
                service_url=service_url,
                path=full_path,
                method=request.method,
                headers=headers,
                data=request.get_data(),
                params=request.args
            )
        
        # Logger succès
        logger.info(f"{request.method} {full_path} -> {service_url} [{response.status_code}]")
        
        # Retourner réponse
        return Response(
            response.content,
            status=response.status_code,
            headers=dict(response.headers)
        )
    
    except Exception as e:
        logger.error(f"Gateway error for {full_path}: {str(e)}")
        return jsonify({
            'error': 'Service temporarily unavailable',
            'details': str(e)
        }), 503

if __name__ == '__main__':
    app.run(host='0.0.0.0', port=5000, debug=False)
"""

# Fichier: gateway/requirements.txt
"""
Flask==3.0.0
Flask-CORS==4.0.0
requests==2.31.0
PyJWT==2.8.0
redis==5.0.1
"""

# ============================================================================
# 8. LANCER L'APPLICATION COMPLÈTE
# ============================================================================

# ÉTAPE 1: Cloner/Créer la structure
"""
mkdir ecommerce-microservices
cd ecommerce-microservices

# Créer tous les dossiers
mkdir -p gateway users-service products-service orders-service notifications-service shared

# Copier tous les fichiers ci-dessus dans leurs dossiers respectifs
"""

# ÉTAPE 2: Créer fichier .env
"""
# Copier le contenu du .env du début
"""

# ÉTAPE 3: Build et lancer avec Docker Compose
"""
docker-compose up --build -d
"""

# ÉTAPE 4: Attendre que tous les services soient UP
"""
# Vérifier logs
docker-compose logs -f

# Attendre "healthy" pour tous
docker-compose ps
"""

# ÉTAPE 5: Initialiser les bases de données
"""
# Users Service
docker-compose exec users-service flask db init
docker-compose exec users-service flask db migrate
docker-compose exec users-service flask db upgrade

# Orders Service
docker-compose exec orders-service flask db init
docker-compose exec orders-service flask db migrate
docker-compose exec orders-service flask db upgrade
"""

# ÉTAPE 6: Tester l'application!

# Test 1: Health checks
"""
curl http://localhost:5000/health
curl http://localhost:5000/health/services
"""

# Test 2: Inscription
"""
curl -X POST http://localhost:5000/api/register \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john@example.com",
    "name": "John Doe",
    "password": "secret123"
  }'
"""

# Test 3: Login
"""
curl -X POST http://localhost:5000/api/login \
  -H "Content-Type: application/json" \
  -d '{
    "email": "john@example.com",
    "password": "secret123"
  }'

# Sauvegarder le token retourné
TOKEN="eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."
"""

# Test 4: Créer produits (via Products Service directement pour populer)
"""
curl -X POST http://localhost:5000/api/products \
  -H "Content-Type: application/json" \
  -d '{
    "name": "iPhone 15",
    "price": 999.99,
    "category": "Electronics",
    "stock": 50,
    "description": "Latest iPhone"
  }'

curl -X POST http://localhost:5000/api/products \
  -H "Content-Type: application/json" \
  -d '{
    "name": "MacBook Pro",
    "price": 2499.99,
    "category": "Electronics",
    "stock": 20,
    "description": "Powerful laptop"
  }'
"""

# Test 5: Lister produits
"""
curl http://localhost:5000/api/products
"""

# Test 6: Créer commande (authentification requise)
"""
curl -X POST http://localhost:5000/api/orders \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $TOKEN" \
  -d '{
    "items": [
      {
        "product_id": "PRODUCT_ID_HERE",
        "quantity": 2
      }
    ]
  }'
"""

# Test 7: Récupérer mes commandes
"""
curl http://localhost:5000/api/users/me \
  -H "Authorization: Bearer $TOKEN"
"""

# Test 8: Vérifier notifications
"""
docker-compose logs notifications-worker
# Vous devriez voir: "Sending order_created notification to user..."
"""

# ============================================================================
# 9. BONNES PRATIQUES IMPLÉMENTÉES
# ============================================================================

"""
[OK] ARCHITECTURE:
- Séparation claire des responsabilités
- Database per service
- API Gateway comme point d'entrée unique
- Communication asynchrone (Celery + RabbitMQ)

[OK] SÉCURITÉ:
- JWT authentication
- Auth centralisée au gateway
- Propagation user info via headers
- Rate limiting par IP
- Validation des données

[OK] RÉSILIENCE:
- Circuit breaker pattern
- Retry automatique
- SAGA pattern pour transactions distribuées
- Compensation en cas d'échec
- Health checks

[OK] PERFORMANCE:
- Cache Redis
- Async notifications
- Connection pooling
- Pagination

[OK] OBSERVABILITÉ:
- Structured logging
- Health checks détaillés
- Ready pour Prometheus metrics
- Correlation ID (peut être ajouté)

[OK] DÉVELOPPEMENT:
- Docker Compose pour environnement complet
- Variables d'environnement
- Migrations de base de données
- Code modulaire et réutilisable
"""

# ============================================================================
# 10. COMMANDES UTILES
# ============================================================================

"""
# Voir logs d'un service
docker-compose logs -f users-service

# Redémarrer un service
docker-compose restart orders-service

# Scaler un service (plusieurs instances)
docker-compose up -d --scale users-service=3

# Accéder à un container
docker-compose exec users-service bash

# Voir stats ressources
docker-compose stats

# Arrêter tout
docker-compose down

# Arrêter et supprimer volumes ([ATTENTION] perte de données)
docker-compose down -v

# Rebuild un service spécifique
docker-compose up -d --build users-service
"""

# ============================================================================
# [BRAVO] FÉLICITATIONS!
# ============================================================================

"""
Vous avez maintenant une application e-commerce complète avec microservices:

[OK] 5 services (Gateway, Users, Products, Orders, Notifications)
[OK] 4 bases de données (PostgreSQL × 2, MongoDB, Redis)
[OK] Message broker (RabbitMQ)
[OK] API Gateway avec auth, rate limiting, circuit breaker
[OK] SAGA pattern pour transactions distribuées
[OK] Cache distribué
[OK] Notifications asynchrones
[OK] Docker Compose pour orchestration
[OK] Prêt pour production avec quelques ajustements

PROCHAINES AMÉLIORATIONS:
- Ajouter Kubernetes pour orchestration production
- Ajouter Prometheus + Grafana pour monitoring
- Ajouter ELK Stack pour logs centralisés
- Ajouter tests (unitaires, intégration)
- Ajouter CI/CD pipeline
- Ajouter documentation OpenAPI/Swagger
- Ajouter versioning API
- Ajouter mécanisme de retry plus sophistiqué
- Ajouter distributed tracing (Jaeger)
- Sécuriser avec HTTPS/TLS

RESSOURCES:
- Flask Docs: https://flask.palletsprojects.com/
- Docker Compose: https://docs.docker.com/compose/
- Microservices Patterns: https://microservices.io/
- Martin Fowler: https://martinfowler.com/microservices/
"""


# === FIN DU CHEATSHEET ===