package services

import (
	"bytes"
	"context"
	"crypto/md5"
	"fmt"
	"io"
	"mime/multipart"
	"net/http"
	"path/filepath"
	"strings"
	"time"

	"github.com/dutchcoders/go-clamd"
	"go.uber.org/zap"
)

// UploadValidator service pour valider les uploads de fichiers
type UploadValidator struct {
	logger                       *zap.Logger
	clamdClient                  *clamd.Clamd
	quarantineDir                string
	clamAVRequiredButUnavailable bool // MOD-P1-001-REFINEMENT: Flag pour fail-secure localisé
	clamAVRequired               bool // MOD-P1-002: Si false, accepte uploads même si ClamAV down
}

// UploadConfig configuration pour les uploads
type UploadConfig struct {
	// Limites de taille
	MaxAudioSize int64 // 100MB
	MaxImageSize int64 // 10MB
	MaxVideoSize int64 // 500MB

	// Types MIME autorisés
	AllowedAudioTypes []string
	AllowedImageTypes []string
	AllowedVideoTypes []string

	// Configuration ClamAV
	ClamAVEnabled  bool // Active/désactive ClamAV
	ClamAVRequired bool // MOD-P1-002: Si false, accepte uploads même si ClamAV down (mode dégradé)
	ClamAVAddress  string

	// Dossier de quarantaine
	QuarantineDir string
}

// DefaultUploadConfig retourne la configuration par défaut
func DefaultUploadConfig() *UploadConfig {
	return &UploadConfig{
		MaxAudioSize: 100 * 1024 * 1024, // 100MB
		MaxImageSize: 10 * 1024 * 1024,  // 10MB
		MaxVideoSize: 500 * 1024 * 1024, // 500MB

		AllowedAudioTypes: []string{
			"audio/mpeg",
			"audio/mp3",
			"audio/wav",
			"audio/wave", // Alias valide pour WAV (http.DetectContentType retourne audio/wave)
			"audio/flac",
			"audio/aac",
			"audio/ogg",
			"audio/m4a",
		},
		AllowedImageTypes: []string{
			"image/jpeg",
			"image/png",
			"image/gif",
			"image/webp",
			"image/svg+xml",
		},
		AllowedVideoTypes: []string{
			"video/mp4",
			"video/webm",
			"video/ogg",
			"video/avi",
		},

		ClamAVEnabled:  true,
		ClamAVRequired: true, // MOD-P1-002: Par défaut, ClamAV est requis (fail-secure)
		ClamAVAddress:  "localhost:3310",
		QuarantineDir:  "/quarantine",
	}
}

// NewUploadValidator crée un nouveau validateur d'upload
// MOD-P1-001-REFINEMENT: Fail-secure localisé - serveur démarre même si ClamAV down,
// mais uploads seront rejetés lors de la validation
func NewUploadValidator(config *UploadConfig, logger *zap.Logger) (*UploadValidator, error) {
	fmt.Printf("🔧 [UPLOAD VALIDATOR] Initialisation - ClamAVEnabled=%v, ClamAVRequired=%v\n", config.ClamAVEnabled, config.ClamAVRequired)

	// EARLY RETURN: Si ClamAV est désactivé, ne JAMAIS toucher au réseau ClamAV
	if !config.ClamAVEnabled {
		fmt.Printf("✅ [UPLOAD VALIDATOR] ClamAV désactivé - Aucune connexion réseau ne sera tentée\n")
		logger.Info("ClamAV is disabled - virus scanning will be skipped",
			zap.Bool("clamav_enabled", config.ClamAVEnabled),
			zap.Bool("clamav_required", config.ClamAVRequired),
		)
		return &UploadValidator{
			logger:                       logger,
			clamdClient:                  nil, // Explicitement nil
			quarantineDir:                config.QuarantineDir,
			clamAVRequiredButUnavailable: false,
			clamAVRequired:               config.ClamAVRequired,
		}, nil
	}

	// ClamAV est activé - initialiser le client
	fmt.Printf("🛡️ [UPLOAD VALIDATOR] ClamAV activé - Initialisation client sur %s\n", config.ClamAVAddress)
	var clamdClient *clamd.Clamd
	clamAVRequiredButUnavailable := false

	// Créer le client ClamAV (cela ne devrait PAS faire de connexion immédiate)
	clamdClient = clamd.NewClamd(config.ClamAVAddress)
	fmt.Printf("🔌 [UPLOAD VALIDATOR] Client ClamAV créé - Test de connexion (Ping avec timeout 2s)...\n")

	// Test connection avec timeout pour éviter blocage - MOD-P1-001-REFINEMENT: Ne pas bloquer le démarrage
	// Créer un contexte avec timeout court pour le Ping
	pingCtx, pingCancel := context.WithTimeout(context.Background(), 2*time.Second)
	defer pingCancel()

	// Lancer le Ping dans une goroutine avec timeout
	pingDone := make(chan error, 1)
	go func() {
		pingDone <- clamdClient.Ping()
	}()

	var err error
	select {
	case err = <-pingDone:
		// Ping terminé (succès ou erreur)
		fmt.Printf("🔌 [UPLOAD VALIDATOR] Ping terminé - erreur: %v\n", err)
	case <-pingCtx.Done():
		// Timeout - ClamAV ne répond pas
		err = fmt.Errorf("ClamAV ping timeout after 2s: %w", pingCtx.Err())
		fmt.Printf("⏱️ [UPLOAD VALIDATOR] Ping timeout - ClamAV ne répond pas\n")
	}

	if err != nil {
		fmt.Printf("⚠️ [UPLOAD VALIDATOR] ClamAV Ping échoué: %v\n", err)
		if config.ClamAVRequired {
			// MOD-P1-002: Si ClamAV est requis, rejeter les uploads (fail-secure)
			logger.Warn("ClamAV is enabled and required but unavailable - uploads will be rejected until ClamAV is available",
				zap.Error(err),
				zap.String("address", config.ClamAVAddress),
			)
			clamAVRequiredButUnavailable = true
		} else {
			// MOD-P1-002: Si ClamAV n'est pas requis, accepter uploads avec warning (mode dégradé)
			logger.Warn("ClamAV is enabled but unavailable and not required - uploads will be accepted without virus scanning (degraded mode)",
				zap.Error(err),
				zap.String("address", config.ClamAVAddress),
				zap.String("security_warning", "Virus scanning is disabled. This should only be used in development or with alternative security measures."),
			)
			clamAVRequiredButUnavailable = false
		}
		// Ne pas retourner d'erreur - le serveur peut démarrer
	} else {
		fmt.Printf("✅ [UPLOAD VALIDATOR] ClamAV Ping réussi - Connexion OK\n")
		logger.Info("ClamAV connection successful")
	}

	fmt.Printf("✅ [UPLOAD VALIDATOR] Validateur initialisé - clamdClient=%v, requiredButUnavailable=%v\n",
		clamdClient != nil, clamAVRequiredButUnavailable)

	return &UploadValidator{
		logger:                       logger,
		clamdClient:                  clamdClient,
		quarantineDir:                config.QuarantineDir,
		clamAVRequiredButUnavailable: clamAVRequiredButUnavailable,
		clamAVRequired:               config.ClamAVRequired,
	}, nil
}

// ValidationResult résultat de la validation
type ValidationResult struct {
	Valid       bool
	FileType    string
	FileSize    int64
	Checksum    string
	Error       string
	Quarantined bool
}

// ValidateFile valide un fichier uploadé
// MOD-P1-001: Le scan ClamAV se fait AVANT toute persistance
func (uv *UploadValidator) ValidateFile(ctx context.Context, fileHeader *multipart.FileHeader, fileType string) (*ValidationResult, error) {
	// DEBUG: Log de début de validation
	fmt.Printf("🚀 [UPLOAD VALIDATE] Début validation fichier: %s (taille: %d bytes, type: %s)\n", fileHeader.Filename, fileHeader.Size, fileType)
	fmt.Printf("🔍 [UPLOAD VALIDATE] État ClamAV - client=%v, required=%v, requiredButUnavailable=%v\n",
		uv.clamdClient != nil, uv.clamAVRequired, uv.clamAVRequiredButUnavailable)

	// EARLY CHECK: Si ClamAV est complètement désactivé, on skip immédiatement
	if uv.clamdClient == nil {
		fmt.Printf("⏭️ [UPLOAD VALIDATE] ClamAV désactivé (client=nil) - scan antivirus ignoré\n")
	}

	result := &ValidationResult{
		FileSize: fileHeader.Size,
	}

	// Ouvrir le fichier
	file, err := fileHeader.Open()
	if err != nil {
		fmt.Printf("❌ [UPLOAD] Erreur ouverture fichier: %v\n", err)
		result.Error = "Failed to open file"
		return result, err
	}
	defer file.Close()
	fmt.Printf("✅ [UPLOAD] Fichier ouvert avec succès\n")

	// BE-SEC-010: Enhanced file validation - read magic bytes for robust type detection
	header := make([]byte, 512)
	n, err := file.Read(header)
	if err != nil && err != io.EOF {
		result.Error = "Failed to read file header"
		return result, err
	}

	if n < 4 {
		result.Error = "File too small to validate"
		return result, nil
	}

	// Reset la position du fichier
	file.Seek(0, 0)

	// BE-SEC-010: Validate magic bytes first (more secure than MIME type alone)
	if err := uv.validateMagicBytes(header[:n], fileType); err != nil {
		result.Error = fmt.Sprintf("Invalid file signature: %v", err)
		return result, nil
	}

	// Détecter le type MIME réel
	detectedMIME := http.DetectContentType(header[:n])
	result.FileType = detectedMIME

	// Valider le type de fichier (double-check with MIME type)
	if !uv.isValidFileType(detectedMIME, fileType) {
		result.Error = fmt.Sprintf("Invalid file type: %s (does not match expected %s)", detectedMIME, fileType)
		return result, nil
	}

	// Valider la taille
	if !uv.isValidFileSize(fileHeader.Size, fileType) {
		result.Error = fmt.Sprintf("File too large for type %s", fileType)
		return result, nil
	}

	// Calculer le checksum MD5
	hash := md5.New()
	file.Seek(0, 0)
	if _, err := io.Copy(hash, file); err != nil {
		result.Error = "Failed to calculate checksum"
		return result, err
	}
	result.Checksum = fmt.Sprintf("%x", hash.Sum(nil))

	// MOD-P1-001: Fail-secure - rejeter upload si ClamAV requis mais indisponible
	// MOD-P1-002: Si ClamAV n'est pas requis, accepter uploads même si ClamAV down (mode dégradé)
	// Cette vérification se fait AVANT le scan pour éviter toute persistance
	if uv.clamAVRequiredButUnavailable && uv.clamAVRequired {
		result.Error = "Virus scanning service is temporarily unavailable. Uploads are disabled for security reasons."
		// Retourner une erreur spéciale pour que le handler puisse retourner 503
		return result, fmt.Errorf("clamav_unavailable: %s", result.Error)
	}

	// MOD-P1-002: Si ClamAV est down mais non requis, logger un warning mais continuer
	if uv.clamAVRequiredButUnavailable && !uv.clamAVRequired {
		uv.logger.Warn("ClamAV unavailable but not required - accepting upload without virus scan (degraded mode)",
			zap.String("filename", fileHeader.Filename),
			zap.String("security_warning", "File accepted without virus scanning. This should only be used in development."),
		)
		// Continuer sans scan ClamAV
	}

	// Scanner avec ClamAV si disponible
	if uv.clamdClient != nil {
		fmt.Printf("🛡️ [UPLOAD VALIDATE] ClamAV activé - Début scan antivirus...\n")
		file.Seek(0, 0)
		scanResult, err := uv.scanWithClamAV(ctx, file)
		if err != nil {
			fmt.Printf("❌ [UPLOAD] Erreur scan ClamAV: %v\n", err)
			uv.logger.Error("ClamAV scan failed",
				zap.Error(err),
				zap.String("filename", fileHeader.Filename),
			)
			// MOD-P1-001: En cas d'erreur de scan (timeout, connexion, etc.), rejeter (fail-secure)
			result.Error = fmt.Sprintf("Virus scan failed: %v", err)
			return result, fmt.Errorf("clamav_scan_error: %w", err)
		}

		// MOD-P1-001: Si virus détecté, rejeter immédiatement (code 422)
		if scanResult != nil && scanResult.Status != "OK" {
			fmt.Printf("⚠️ [UPLOAD] Virus détecté: %s\n", scanResult.Description)
			result.Quarantined = true
			result.Error = fmt.Sprintf("Virus detected: %s", scanResult.Description)
			uv.logger.Warn("Virus detected in uploaded file",
				zap.String("filename", fileHeader.Filename),
				zap.String("virus", scanResult.Description),
			)
			return result, fmt.Errorf("clamav_infected: %s", scanResult.Description)
		}

		fmt.Printf("✅ [UPLOAD VALIDATE] Scan ClamAV réussi - fichier propre\n")
		uv.logger.Info("File scanned successfully with ClamAV",
			zap.String("filename", fileHeader.Filename),
			zap.String("status", "clean"),
		)
	} else {
		fmt.Printf("⏭️ [UPLOAD VALIDATE] ClamAV désactivé (client=nil) - scan ignoré, validation continue\n")
	}

	// Valider l'extension du fichier
	ext := strings.ToLower(filepath.Ext(fileHeader.Filename))
	if !uv.isValidExtension(ext, fileType) {
		result.Error = fmt.Sprintf("Invalid file extension: %s", ext)
		return result, nil
	}

	result.Valid = true
	fmt.Printf("✅ [UPLOAD VALIDATE] Validation complète - fichier accepté (Valid=true)\n")
	return result, nil
}

// isValidFileType vérifie si le type MIME est autorisé
func (uv *UploadValidator) isValidFileType(mimeType, fileType string) bool {
	config := DefaultUploadConfig()

	switch fileType {
	case "audio":
		for _, allowed := range config.AllowedAudioTypes {
			if mimeType == allowed {
				return true
			}
		}
	case "image":
		for _, allowed := range config.AllowedImageTypes {
			if mimeType == allowed {
				return true
			}
		}
	case "video":
		for _, allowed := range config.AllowedVideoTypes {
			if mimeType == allowed {
				return true
			}
		}
	}

	return false
}

// isValidFileSize vérifie si la taille du fichier est autorisée
func (uv *UploadValidator) isValidFileSize(size int64, fileType string) bool {
	config := DefaultUploadConfig()

	switch fileType {
	case "audio":
		return size <= config.MaxAudioSize
	case "image":
		return size <= config.MaxImageSize
	case "video":
		return size <= config.MaxVideoSize
	}

	return false
}

// isValidExtension vérifie si l'extension est valide pour le type
func (uv *UploadValidator) isValidExtension(ext, fileType string) bool {
	extensions := map[string][]string{
		"audio": {".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a"},
		"image": {".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"},
		"video": {".mp4", ".webm", ".ogg", ".avi"},
	}

	if allowedExts, exists := extensions[fileType]; exists {
		for _, allowed := range allowedExts {
			if ext == allowed {
				return true
			}
		}
	}

	return false
}

// BE-SEC-010: validateMagicBytes validates file signature (magic bytes) to prevent file type spoofing
func (uv *UploadValidator) validateMagicBytes(header []byte, fileType string) error {
	if len(header) < 4 {
		return fmt.Errorf("insufficient data for magic byte validation")
	}

	switch fileType {
	case "audio":
		return uv.validateAudioMagicBytes(header)
	case "image":
		return uv.validateImageMagicBytes(header)
	case "video":
		return uv.validateVideoMagicBytes(header)
	default:
		return fmt.Errorf("unknown file type: %s", fileType)
	}
}

// validateAudioMagicBytes validates audio file signatures
func (uv *UploadValidator) validateAudioMagicBytes(header []byte) error {
	// MP3: ID3v2 tag (starts with "ID3") or MPEG frame sync (0xFF followed by 0xFB/0xF3/0xF2/0xE0+)
	if len(header) >= 3 && string(header[:3]) == "ID3" {
		return nil
	}
	if len(header) >= 2 && header[0] == 0xFF && (header[1] == 0xFB || header[1] == 0xF3 || header[1] == 0xF2 || (header[1]&0xE0) == 0xE0) {
		return nil
	}

	// FLAC: "fLaC" at offset 0 or 4 (if RIFF wrapper)
	if len(header) >= 4 && string(header[:4]) == "fLaC" {
		return nil
	}
	if len(header) >= 8 && string(header[:4]) == "RIFF" && string(header[8:12]) == "fLaC" {
		return nil
	}

	// WAV: "RIFF" followed by "WAVE"
	if len(header) >= 12 && string(header[:4]) == "RIFF" && string(header[8:12]) == "WAVE" {
		return nil
	}

	// OGG: "OggS"
	if len(header) >= 4 && string(header[:4]) == "OggS" {
		return nil
	}

	// AAC/M4A: Can start with various patterns, check for common ones
	// M4A files often start with "ftyp" at offset 4
	if len(header) >= 8 && string(header[4:8]) == "ftyp" {
		return nil
	}
	// Some AAC files start with 0xFF 0xF1 or 0xFF 0xF9
	if len(header) >= 2 && header[0] == 0xFF && (header[1] == 0xF1 || header[1] == 0xF9) {
		return nil
	}

	return fmt.Errorf("invalid audio file signature")
}

// validateImageMagicBytes validates image file signatures
func (uv *UploadValidator) validateImageMagicBytes(header []byte) error {
	// JPEG: 0xFF 0xD8 0xFF
	if len(header) >= 3 && header[0] == 0xFF && header[1] == 0xD8 && header[2] == 0xFF {
		return nil
	}

	// PNG: 0x89 0x50 0x4E 0x47 0x0D 0x0A 0x1A 0x0A
	if len(header) >= 8 && header[0] == 0x89 && header[1] == 0x50 && header[2] == 0x4E && header[3] == 0x47 &&
		header[4] == 0x0D && header[5] == 0x0A && header[6] == 0x1A && header[7] == 0x0A {
		return nil
	}

	// GIF: "GIF87a" or "GIF89a"
	if len(header) >= 6 && (string(header[:6]) == "GIF87a" || string(header[:6]) == "GIF89a") {
		return nil
	}

	// WebP: "RIFF" followed by "WEBP" at offset 8
	if len(header) >= 12 && string(header[:4]) == "RIFF" && string(header[8:12]) == "WEBP" {
		return nil
	}

	// SVG: XML declaration or <svg tag (text-based, check for XML/SVG markers)
	if len(header) >= 5 && string(header[:5]) == "<?xml" {
		// Check if it contains SVG
		headerStr := string(header)
		if strings.Contains(headerStr, "<svg") || strings.Contains(headerStr, "svg") {
			return nil
		}
	}
	if len(header) >= 4 && strings.HasPrefix(strings.ToLower(string(header)), "<svg") {
		return nil
	}

	return fmt.Errorf("invalid image file signature")
}

// validateVideoMagicBytes validates video file signatures
func (uv *UploadValidator) validateVideoMagicBytes(header []byte) error {
	// MP4: "ftyp" at offset 4 (after 4-byte size field)
	if len(header) >= 8 && string(header[4:8]) == "ftyp" {
		return nil
	}

	// WebM: "1A 45 DF A3" (EBML header)
	if len(header) >= 4 && header[0] == 0x1A && header[1] == 0x45 && header[2] == 0xDF && header[3] == 0xA3 {
		return nil
	}

	// OGG: "OggS" (OGG container, can contain video)
	if len(header) >= 4 && string(header[:4]) == "OggS" {
		return nil
	}

	// AVI: "RIFF" followed by "AVI " at offset 8
	if len(header) >= 12 && string(header[:4]) == "RIFF" && string(header[8:12]) == "AVI " {
		return nil
	}

	return fmt.Errorf("invalid video file signature")
}

// scanWithClamAV scanne le fichier avec ClamAV avec timeout strict
// MOD-P1-001: Timeout strict via context pour éviter les blocages
func (uv *UploadValidator) scanWithClamAV(ctx context.Context, file io.Reader) (*clamd.ScanResult, error) {
	// Timeout strict: 30 secondes max pour le scan
	scanCtx, cancel := context.WithTimeout(ctx, 30*time.Second)
	defer cancel()

	// Lire tout le fichier en mémoire pour le scan
	var buf bytes.Buffer
	if _, err := io.Copy(&buf, file); err != nil {
		return nil, fmt.Errorf("failed to read file for scan: %w", err)
	}

	// Scanner avec ClamAV - ScanStream retourne (chan *ScanResult, error)
	controlChan := make(chan bool, 1)
	responseChan, err := uv.clamdClient.ScanStream(&buf, controlChan)
	if err != nil {
		return nil, fmt.Errorf("clamav scan initialization failed: %w", err)
	}

	// Lire le premier résultat avec timeout
	select {
	case result := <-responseChan:
		if result == nil {
			// Pas de résultat = OK (pas de virus détecté)
			return &clamd.ScanResult{Status: "OK"}, nil
		}
		return result, nil
	case <-scanCtx.Done():
		// Timeout: annuler le scan
		controlChan <- true
		return nil, fmt.Errorf("clamav scan timeout: %w", scanCtx.Err())
	}
}

// QuarantineFile met un fichier en quarantaine
func (uv *UploadValidator) QuarantineFile(fileHeader *multipart.FileHeader, reason string) error {
	// Créer le nom de fichier avec timestamp
	timestamp := time.Now().Format("20060102_150405")
	filename := fmt.Sprintf("%s_%s_%s", timestamp, fileHeader.Filename, reason)
	quarantinePath := filepath.Join(uv.quarantineDir, filename)

	// Ouvrir le fichier source
	srcFile, err := fileHeader.Open()
	if err != nil {
		return fmt.Errorf("failed to open source file: %w", err)
	}
	defer srcFile.Close()

	// Créer le fichier de quarantaine
	// Note: Dans un vrai environnement, il faudrait créer le dossier s'il n'existe pas
	// et gérer les permissions appropriées

	uv.logger.Warn("File quarantined",
		zap.String("original_name", fileHeader.Filename),
		zap.String("quarantine_path", quarantinePath),
		zap.String("reason", reason),
	)

	return nil
}

// GetFileTypeFromPath détermine le type de fichier à partir du chemin
func (uv *UploadValidator) GetFileTypeFromPath(filename string) string {
	ext := strings.ToLower(filepath.Ext(filename))

	audioExts := []string{".mp3", ".wav", ".flac", ".aac", ".ogg", ".m4a"}
	imageExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp", ".svg"}
	videoExts := []string{".mp4", ".webm", ".ogg", ".avi"}

	for _, audioExt := range audioExts {
		if ext == audioExt {
			return "audio"
		}
	}

	for _, imageExt := range imageExts {
		if ext == imageExt {
			return "image"
		}
	}

	for _, videoExt := range videoExts {
		if ext == videoExt {
			return "video"
		}
	}

	return "unknown"
}
