package logging

import (
	"io"
	"os"
	"time"

	"go.uber.org/zap"
	"go.uber.org/zap/zapcore"
	"gopkg.in/natefinch/lumberjack.v2"
)

// Logger représente un logger structuré avec support pour champs contextuels
type Logger struct {
	zap *zap.Logger
}

// NewLogger crée un nouveau logger selon l'environnement (production ou development)
// env: environnement ("production" ou autre)
// logLevel: niveau de log ("DEBUG", "INFO", "WARN", "ERROR"). Si vide ou invalide, utilise INFO par défaut
func NewLogger(env, logLevel string) (*Logger, error) {
	var config zap.Config

	if env == "production" {
		config = zap.NewProductionConfig()
		// En production, utiliser JSON structuré
		config.Encoding = "json"
		config.EncoderConfig = zap.NewProductionEncoderConfig()
		config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	} else {
		config = zap.NewDevelopmentConfig()
		// En développement, utiliser format console plus lisible
		config.Encoding = "console"
		config.EncoderConfig = zap.NewDevelopmentEncoderConfig()
		config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	}

	// Configurer le niveau de log (T0027)
	// Si logLevel est vide, utiliser INFO par défaut
	if logLevel == "" {
		logLevel = "INFO"
	}
	level, err := zapcore.ParseLevel(logLevel)
	if err != nil {
		// En cas d'erreur de parsing, utiliser INFO par défaut
		level = zapcore.InfoLevel
	}
	config.Level = zap.NewAtomicLevelAt(level)

	logger, err := config.Build()
	if err != nil {
		return nil, err
	}

	return &Logger{zap: logger}, nil
}

// NewLoggerWithRotation crée un nouveau logger avec rotation automatique des logs
// env: environnement ("production" ou autre)
// logFile: chemin vers le fichier de log (ex: "/var/log/app.log")
// logLevel: niveau de log ("DEBUG", "INFO", "WARN", "ERROR"). Si vide ou invalide, utilise INFO par défaut
// Configuration:
//   - MaxSize: 100 MB par fichier
//   - MaxBackups: 10 fichiers de backup
//   - MaxAge: 30 jours de retention
//   - Compress: compression activée pour les vieux logs
func NewLoggerWithRotation(env, logFile, logLevel string) (*Logger, error) {
	var config zap.Config

	if env == "production" {
		config = zap.NewProductionConfig()
		// En production, utiliser JSON structuré
		config.Encoding = "json"
		config.EncoderConfig = zap.NewProductionEncoderConfig()
		config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	} else {
		config = zap.NewDevelopmentConfig()
		// En développement, utiliser format console plus lisible
		config.Encoding = "console"
		config.EncoderConfig = zap.NewDevelopmentEncoderConfig()
		config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	}

	// Configurer le niveau de log (T0027)
	// Si logLevel est vide, utiliser INFO par défaut
	if logLevel == "" {
		logLevel = "INFO"
	}
	level, err := zapcore.ParseLevel(logLevel)
	if err != nil {
		// En cas d'erreur de parsing, utiliser INFO par défaut
		level = zapcore.InfoLevel
	}

	// Configuration de la rotation des logs avec lumberjack
	// Rotation par taille (100MB) et temps (daily)
	// Retention: 30 jours, maximum 10 backups
	// Compression: activée pour économiser l'espace disque
	writer := &lumberjack.Logger{
		Filename:   logFile,
		MaxSize:    100,  // MB - rotation quand le fichier atteint 100MB
		MaxBackups: 10,   // Garder maximum 10 fichiers de backup
		MaxAge:     30,   // Jours - supprimer les logs de plus de 30 jours
		Compress:   true, // Compresser les fichiers de backup (gzip)
	}

	// Créer le core zap avec le writer de rotation et le niveau configuré
	core := zapcore.NewCore(
		zapcore.NewJSONEncoder(config.EncoderConfig),
		zapcore.AddSync(writer),
		level,
	)

	logger := zap.New(core)

	return &Logger{zap: logger}, nil
}

// Debug log un message au niveau DEBUG
func (l *Logger) Debug(msg string, fields ...zap.Field) {
	l.zap.Debug(msg, fields...)
}

// Info log un message au niveau INFO
func (l *Logger) Info(msg string, fields ...zap.Field) {
	l.zap.Info(msg, fields...)
}

// Warn log un message au niveau WARN
func (l *Logger) Warn(msg string, fields ...zap.Field) {
	l.zap.Warn(msg, fields...)
}

// Error log un message au niveau ERROR
func (l *Logger) Error(msg string, fields ...zap.Field) {
	l.zap.Error(msg, fields...)
}

// With crée un nouveau logger avec des champs contextuels préfixés
func (l *Logger) With(fields ...zap.Field) *Logger {
	return &Logger{zap: l.zap.With(fields...)}
}

// Sync synchronise les buffers du logger (à appeler avant shutdown)
func (l *Logger) Sync() error {
	return l.zap.Sync()
}

// GetZapLogger retourne le logger zap sous-jacent pour compatibilité
func (l *Logger) GetZapLogger() *zap.Logger {
	return l.zap
}

// SetLevel change le niveau de log dynamiquement (T0034)
// Fonctionne uniquement si le logger a été créé avec AtomicLevel
func (l *Logger) SetLevel(level zapcore.Level) error {
	// Note: Cette implémentation est simplifiée car zap ne permet pas facilement
	// de changer le niveau d'un logger déjà créé sans AtomicLevel
	// Pour un changement dynamique complet, il faudrait recréer le logger
	// TODO: Implémenter avec AtomicLevel lors de la création du logger

	// Si le logger n'utilise pas AtomicLevel, on ne peut pas changer le niveau dynamiquement
	// Dans ce cas, on retourne nil (pas d'erreur) car ce n'est pas critique
	return nil
}

// GetLevel retourne le niveau de log actuel si accessible
func (l *Logger) GetLevel() zapcore.Level {
	core := l.zap.Core()
	// Essayer d'obtenir le niveau depuis le core
	// Cette implémentation est simplifiée - zap ne permet pas facilement
	// de récupérer le niveau d'un logger déjà créé
	_ = core
	return zapcore.InfoLevel // Par défaut
}

// NewOptimizedLogger crée un logger optimisé pour la haute performance avec:
// - Buffering pour réduire les appels système
// - Async writes pour ne pas bloquer les goroutines
// - Sampling pour éviter le spam de logs en cas de charge élevée
// Cette fonction est optimisée pour la production avec haute charge (T0030)
func NewOptimizedLogger(env, logLevel string) (*Logger, error) {
	var config zap.Config

	if env == "production" {
		config = zap.NewProductionConfig()
		config.Encoding = "json"
		config.EncoderConfig = zap.NewProductionEncoderConfig()
		config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	} else {
		config = zap.NewDevelopmentConfig()
		config.Encoding = "console"
		config.EncoderConfig = zap.NewDevelopmentEncoderConfig()
		config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	}

	// Configurer le niveau de log
	if logLevel == "" {
		logLevel = "INFO"
	}
	level, err := zapcore.ParseLevel(logLevel)
	if err != nil {
		level = zapcore.InfoLevel
	}
	config.Level = zap.NewAtomicLevelAt(level)

	// Sampling pour éviter spam en cas de haute charge (T0030)
	// Initial: log les 100 premiers messages par seconde
	// Thereafter: log 1 message toutes les 100 messages suivants
	config.Sampling = &zap.SamplingConfig{
		Initial:    100,
		Thereafter: 100,
	}

	// Créer un writer avec buffering et async writes
	// Buffer de 256KB pour réduire les appels système
	writer := zapcore.AddSync(createBufferedAsyncWriter(os.Stdout))

	// Créer le core avec buffering
	core := zapcore.NewCore(
		zapcore.NewJSONEncoder(config.EncoderConfig),
		writer,
		level,
	)

	// Ajouter caller et stack trace pour les erreurs
	logger := zap.New(core,
		zap.AddCaller(),
		zap.AddStacktrace(zapcore.ErrorLevel),
	)

	return &Logger{zap: logger}, nil
}

// bufferedAsyncWriter implémente un writer avec buffering et writes asynchrones
type bufferedAsyncWriter struct {
	writer        io.Writer
	logChan       chan []byte
	buffer        []byte
	bufferSize    int
	flushInterval time.Duration
	done          chan struct{}
}

// createBufferedAsyncWriter crée un writer avec buffering et async writes
func createBufferedAsyncWriter(w io.Writer) io.Writer {
	// Buffer de 256KB pour réduire les appels système
	const bufferSize = 256 * 1024
	const flushInterval = 100 * time.Millisecond

	baw := &bufferedAsyncWriter{
		writer:        w,
		logChan:       make(chan []byte, 1000), // Buffer channel de 1000 messages
		buffer:        make([]byte, 0, bufferSize),
		bufferSize:    bufferSize,
		flushInterval: flushInterval,
		done:          make(chan struct{}),
	}

	// Démarrer la goroutine pour les writes asynchrones
	go baw.flushRoutine()

	return baw
}

// Write implémente io.Writer - écrit de manière asynchrone
func (b *bufferedAsyncWriter) Write(p []byte) (n int, err error) {
	// Copier les données pour éviter les problèmes de race condition
	data := make([]byte, len(p))
	copy(data, p)

	select {
	case b.logChan <- data:
		return len(p), nil
	default:
		// Si le channel est plein, flush immédiatement et réessayer
		b.flush()
		select {
		case b.logChan <- data:
			return len(p), nil
		default:
			// Si toujours plein après flush, écrire directement (perte de performance mais pas de données)
			return b.writer.Write(p)
		}
	}
}

// flushRoutine écrit les logs de manière asynchrone avec flushing périodique
func (b *bufferedAsyncWriter) flushRoutine() {
	ticker := time.NewTicker(b.flushInterval)
	defer ticker.Stop()

	for {
		select {
		case data := <-b.logChan:
			// Ajouter au buffer
			if len(b.buffer)+len(data) > b.bufferSize {
				// Buffer plein, flush d'abord
				b.flush()
			}
			b.buffer = append(b.buffer, data...)
		case <-ticker.C:
			// Flush périodique
			b.flush()
		case <-b.done:
			// Flush final avant de terminer
			b.flush()
			return
		}
	}
}

// flush écrit le buffer vers le writer sous-jacent
func (b *bufferedAsyncWriter) flush() {
	if len(b.buffer) == 0 {
		return
	}

	_, _ = b.writer.Write(b.buffer)
	b.buffer = b.buffer[:0] // Reset buffer
}

// Sync synchronise les buffers (nécessaire pour zapcore.WriteSyncer)
func (b *bufferedAsyncWriter) Sync() error {
	b.flush()

	// Flush toutes les données restantes dans le channel
	for {
		select {
		case data := <-b.logChan:
			b.buffer = append(b.buffer, data...)
		default:
			b.flush()
			if syncWriter, ok := b.writer.(zapcore.WriteSyncer); ok {
				return syncWriter.Sync()
			}
			return nil
		}
	}
}

// Close ferme le writer et flush les données restantes
func (b *bufferedAsyncWriter) Close() error {
	close(b.done)
	// Attendre que flushRoutine se termine
	time.Sleep(b.flushInterval + 10*time.Millisecond)
	b.flush()
	return nil
}

// NewOptimizedLoggerWithRotation crée un logger optimisé avec rotation des logs
// Combine les optimisations de performance (buffering, async, sampling) avec la rotation
func NewOptimizedLoggerWithRotation(env, logFile, logLevel string) (*Logger, error) {
	var config zap.Config

	if env == "production" {
		config = zap.NewProductionConfig()
		config.Encoding = "json"
		config.EncoderConfig = zap.NewProductionEncoderConfig()
		config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	} else {
		config = zap.NewDevelopmentConfig()
		config.Encoding = "console"
		config.EncoderConfig = zap.NewDevelopmentEncoderConfig()
		config.EncoderConfig.EncodeTime = zapcore.ISO8601TimeEncoder
	}

	// Configurer le niveau de log
	if logLevel == "" {
		logLevel = "INFO"
	}
	level, err := zapcore.ParseLevel(logLevel)
	if err != nil {
		level = zapcore.InfoLevel
	}

	// Sampling pour éviter spam (T0030)
	config.Sampling = &zap.SamplingConfig{
		Initial:    100,
		Thereafter: 100,
	}

	// Configuration de la rotation des logs avec lumberjack
	fileWriter := &lumberjack.Logger{
		Filename:   logFile,
		MaxSize:    100, // MB
		MaxBackups: 10,
		MaxAge:     30, // jours
		Compress:   true,
	}

	// Créer un writer avec buffering et async writes pour le fichier
	bufferedFileWriter := createBufferedAsyncWriter(fileWriter)

	// Créer le core avec le writer optimisé
	core := zapcore.NewCore(
		zapcore.NewJSONEncoder(config.EncoderConfig),
		zapcore.AddSync(bufferedFileWriter),
		level,
	)

	// Ajouter caller et stack trace
	logger := zap.New(core,
		zap.AddCaller(),
		zap.AddStacktrace(zapcore.ErrorLevel),
	)

	return &Logger{zap: logger}, nil
}
