package services

import (
	"context"
	"crypto/md5"
	"encoding/hex"
	"fmt"
	"io"
	"mime/multipart"
	"os"
	"path/filepath"
	"time"

	"github.com/google/uuid"
	"github.com/redis/go-redis/v9"
	"go.uber.org/zap"
)

// ChunkUploadInfo représente les informations sur un upload par chunks
// MIGRATION UUID: UserID migré vers uuid.UUID
type ChunkUploadInfo struct {
	UploadID    string            `json:"upload_id"`
	UserID      uuid.UUID         `json:"user_id"`
	TotalChunks int               `json:"total_chunks"`
	TotalSize   int64             `json:"total_size"`
	Filename    string            `json:"filename"`
	Chunks      map[int]ChunkInfo `json:"chunks"`                 // chunk_number -> ChunkInfo
	ReceivedMD5 string            `json:"received_md5,omitempty"` // MD5 du fichier final
	CreatedAt   time.Time         `json:"created_at"`
	UpdatedAt   time.Time         `json:"updated_at"`
}

// ChunkInfo représente les informations sur un chunk
type ChunkInfo struct {
	ChunkNumber int    `json:"chunk_number"`
	Size        int64  `json:"size"`
	MD5         string `json:"md5"`
	FilePath    string `json:"file_path"`
	Received    bool   `json:"received"`
}

// UploadState représente l'état d'un upload pour la reprise
// MIGRATION UUID: UserID migré vers uuid.UUID
type UploadState struct {
	UploadID       string    `json:"upload_id"`
	UserID         uuid.UUID `json:"user_id"`
	TotalChunks    int       `json:"total_chunks"`
	TotalSize      int64     `json:"total_size"`
	Filename       string    `json:"filename"`
	ChunksReceived []int     `json:"chunks_received"` // Liste des numéros de chunks reçus
	LastChunk      int       `json:"last_chunk"`      // Dernier chunk reçu (0 si aucun)
	ReceivedCount  int       `json:"received_count"`  // Nombre de chunks reçus
	Progress       int       `json:"progress"`        // Pourcentage de progression (0-100)
	CreatedAt      time.Time `json:"created_at"`
	UpdatedAt      time.Time `json:"updated_at"`
}

// TrackChunkService gère l'upload par chunks de fichiers audio
type TrackChunkService struct {
	chunksDir       string
	store           UploadStateStore
	logger          *zap.Logger
	cleanupInterval time.Duration
	maxUploadAge    time.Duration
}

// NewTrackChunkService crée un nouveau service de gestion d'upload par chunks
// MIGRATION: Ajout de Redis Client pour le store
func NewTrackChunkService(chunksDir string, redisClient *redis.Client, logger *zap.Logger) *TrackChunkService {
	if chunksDir == "" {
		chunksDir = "uploads/tracks/chunks"
	}
	if logger == nil {
		logger = zap.NewNop()
	}

	// 24h retention for uploads
	store := NewRedisUploadStore(redisClient, 24*time.Hour)

	service := &TrackChunkService{
		chunksDir:       chunksDir,
		store:           store,
		logger:          logger,
		cleanupInterval: 1 * time.Hour,
		maxUploadAge:    24 * time.Hour,
	}

	// Créer le répertoire de chunks
	if err := os.MkdirAll(chunksDir, 0755); err != nil {
		logger.Warn("Failed to create chunks directory", zap.Error(err))
	}

	// Démarrer le nettoyages des FICHIERS orphelins (Garbage Collector)
	go service.startDiskCleanup()

	return service
}

// InitiateChunkedUpload initialise un nouvel upload par chunks
func (s *TrackChunkService) InitiateChunkedUpload(userID uuid.UUID, totalChunks int, totalSize int64, filename string) (string, error) {
	uploadID := uuid.New().String()

	uploadInfo := &ChunkUploadInfo{
		UploadID:    uploadID,
		UserID:      userID,
		TotalChunks: totalChunks,
		TotalSize:   totalSize,
		Filename:    filename,
		Chunks:      make(map[int]ChunkInfo),
		CreatedAt:   time.Now(),
		UpdatedAt:   time.Now(),
	}

	// Save to Redis
	ctx := context.Background()
	if err := s.store.SetState(ctx, uploadInfo); err != nil {
		return "", fmt.Errorf("failed to initiate upload: %w", err)
	}

	s.logger.Info("Chunked upload initiated",
		zap.String("upload_id", uploadID),
		zap.String("user_id", userID.String()),
		zap.Int("total_chunks", totalChunks),
		zap.Int64("total_size", totalSize),
	)

	return uploadID, nil
}

// SaveChunk sauvegarde un chunk reçu
func (s *TrackChunkService) SaveChunk(ctx context.Context, uploadID string, chunkNumber int, totalChunks int, fileHeader *multipart.FileHeader) error {
	// 1. Get State from Redis
	uploadInfo, err := s.store.GetState(ctx, uploadID)
	if err != nil {
		return err
	}

	// Use mutex within memory object is Useless in distributed system,
	// BUT since we just fetched it and will write it back, we rely on Redis being fast.
	// Optimistic locking (WATCH) would be better but simple GET/SET is acceptable for P0 fix
	// assuming low contention per user/upload.

	// Vérifier que le chunk n'a pas déjà été reçu
	if chunk, exists := uploadInfo.Chunks[chunkNumber]; exists && chunk.Received {
		return fmt.Errorf("chunk %d already received", chunkNumber)
	}

	// Vérifier les paramètres
	if uploadInfo.TotalChunks != totalChunks {
		return fmt.Errorf("total chunks mismatch: expected %d, got %d", uploadInfo.TotalChunks, totalChunks)
	}

	// Créer le répertoire pour cet upload
	uploadDir := filepath.Join(s.chunksDir, uploadID)
	if err := os.MkdirAll(uploadDir, 0755); err != nil {
		return fmt.Errorf("failed to create upload directory: %w", err)
	}

	// Sauvegarder le chunk
	chunkPath := filepath.Join(uploadDir, fmt.Sprintf("chunk_%d", chunkNumber))

	file, err := fileHeader.Open()
	if err != nil {
		return fmt.Errorf("failed to open chunk file: %w", err)
	}
	defer file.Close()

	// Créer le fichier de destination
	destFile, err := os.Create(chunkPath)
	if err != nil {
		return fmt.Errorf("failed to create chunk file: %w", err)
	}
	defer destFile.Close()

	// Calculer le MD5 pendant la copie
	hash := md5.New()
	multiWriter := io.MultiWriter(destFile, hash)

	if _, err := io.Copy(multiWriter, file); err != nil {
		os.Remove(chunkPath)
		return fmt.Errorf("failed to save chunk: %w", err)
	}

	chunkMD5 := hex.EncodeToString(hash.Sum(nil))

	// Enregistrer les informations du chunk
	uploadInfo.Chunks[chunkNumber] = ChunkInfo{
		ChunkNumber: chunkNumber,
		Size:        fileHeader.Size,
		MD5:         chunkMD5,
		FilePath:    chunkPath,
		Received:    true,
	}

	uploadInfo.UpdatedAt = time.Now()

	// Update State in Redis
	if err := s.store.SetState(ctx, uploadInfo); err != nil {
		return fmt.Errorf("failed to update upload state: %w", err)
	}

	s.logger.Info("Chunk saved",
		zap.String("upload_id", uploadID),
		zap.Int("chunk_number", chunkNumber),
		zap.Int64("size", fileHeader.Size),
		zap.String("md5", chunkMD5),
	)

	return nil
}

// GetUploadInfo récupère les informations d'un upload
func (s *TrackChunkService) GetUploadInfo(uploadID string) (*ChunkUploadInfo, error) {
	return s.store.GetState(context.Background(), uploadID)
}

// CompleteChunkedUpload assemble tous les chunks et crée le fichier final
func (s *TrackChunkService) CompleteChunkedUpload(ctx context.Context, uploadID string, finalPath string) (string, int64, string, error) {
	// Get State
	uploadInfo, err := s.store.GetState(ctx, uploadID)
	if err != nil {
		return "", 0, "", err
	}

	// Vérifier que tous les chunks ont été reçus
	if len(uploadInfo.Chunks) != uploadInfo.TotalChunks {
		return "", 0, "", fmt.Errorf("missing chunks: received %d/%d", len(uploadInfo.Chunks), uploadInfo.TotalChunks)
	}

	// Vérifier l'ordre des chunks (1 à totalChunks)
	for i := 1; i <= uploadInfo.TotalChunks; i++ {
		chunk, exists := uploadInfo.Chunks[i]
		if !exists || !chunk.Received {
			return "", 0, "", fmt.Errorf("chunk %d is missing", i)
		}
	}

	// Créer le répertoire de destination
	if err := os.MkdirAll(filepath.Dir(finalPath), 0755); err != nil {
		return "", 0, "", fmt.Errorf("failed to create destination directory: %w", err)
	}

	// Assembler les chunks
	finalFile, err := os.Create(finalPath)
	if err != nil {
		return "", 0, "", fmt.Errorf("failed to create final file: %w", err)
	}
	defer finalFile.Close()

	hash := md5.New()
	multiWriter := io.MultiWriter(finalFile, hash)

	var totalSize int64

	// Assembler les chunks dans l'ordre
	for i := 1; i <= uploadInfo.TotalChunks; i++ {
		chunk := uploadInfo.Chunks[i]

		chunkFile, err := os.Open(chunk.FilePath)
		if err != nil {
			finalFile.Close()
			os.Remove(finalPath)
			return "", 0, "", fmt.Errorf("failed to open chunk %d: %w", i, err)
		}

		size, err := io.Copy(multiWriter, chunkFile)
		chunkFile.Close()

		if err != nil {
			finalFile.Close()
			os.Remove(finalPath)
			return "", 0, "", fmt.Errorf("failed to write chunk %d: %w", i, err)
		}

		totalSize += size
	}

	finalMD5 := hex.EncodeToString(hash.Sum(nil))

	// Vérifier la taille totale
	if totalSize != uploadInfo.TotalSize {
		finalFile.Close()
		os.Remove(finalPath)
		return "", 0, "", fmt.Errorf("size mismatch: expected %d, got %d", uploadInfo.TotalSize, totalSize)
	}

	// Nettoyer les chunks temporaires
	uploadDir := filepath.Join(s.chunksDir, uploadID)
	if err := os.RemoveAll(uploadDir); err != nil {
		s.logger.Warn("Failed to cleanup chunks", zap.String("upload_id", uploadID), zap.Error(err))
	}

	// Supprimer l'upload de Redis
	if err := s.store.DeleteState(ctx, uploadID); err != nil {
		s.logger.Warn("Failed to delete state from Redis", zap.Error(err))
	}

	s.logger.Info("Chunked upload completed",
		zap.String("upload_id", uploadID),
		zap.String("final_path", finalPath),
		zap.Int64("total_size", totalSize),
		zap.String("md5", finalMD5),
	)

	return uploadInfo.Filename, totalSize, finalMD5, nil
}

// GetUploadState récupère l'état d'un upload pour permettre la reprise
func (s *TrackChunkService) GetUploadState(uploadID string) (*UploadState, error) {
	uploadInfo, err := s.store.GetState(context.Background(), uploadID)
	if err != nil {
		return nil, err
	}

	// Compter les chunks reçus et déterminer le dernier
	chunksReceived := make([]int, 0, len(uploadInfo.Chunks))
	lastChunk := 0
	receivedCount := 0

	for chunkNum, chunk := range uploadInfo.Chunks {
		if chunk.Received {
			chunksReceived = append(chunksReceived, chunkNum)
			if chunkNum > lastChunk {
				lastChunk = chunkNum
			}
			receivedCount++
		}
	}

	progress := 0
	if uploadInfo.TotalChunks > 0 {
		progress = (receivedCount * 100) / uploadInfo.TotalChunks
	}

	return &UploadState{
		UploadID:       uploadInfo.UploadID,
		UserID:         uploadInfo.UserID,
		TotalChunks:    uploadInfo.TotalChunks,
		TotalSize:      uploadInfo.TotalSize,
		Filename:       uploadInfo.Filename,
		ChunksReceived: chunksReceived,
		LastChunk:      lastChunk,
		ReceivedCount:  receivedCount,
		Progress:       progress,
		CreatedAt:      uploadInfo.CreatedAt,
		UpdatedAt:      uploadInfo.UpdatedAt,
	}, nil
}

// GetUploadProgress retourne la progression d'un upload par chunks
func (s *TrackChunkService) GetUploadProgress(uploadID string) (int, int, error) {
	uploadInfo, err := s.store.GetState(context.Background(), uploadID)
	if err != nil {
		return 0, 0, err
	}

	receivedChunks := 0
	for _, chunk := range uploadInfo.Chunks {
		if chunk.Received {
			receivedChunks++
		}
	}

	progress := (receivedChunks * 100) / uploadInfo.TotalChunks
	return receivedChunks, progress, nil
}

// CleanupUpload supprime un upload et ses chunks
func (s *TrackChunkService) CleanupUpload(uploadID string) error {
	// Clean from Redis
	_ = s.store.DeleteState(context.Background(), uploadID) // Ignore error if already deleted

	// Clean from Disk
	uploadDir := filepath.Join(s.chunksDir, uploadID)
	if err := os.RemoveAll(uploadDir); err != nil {
		return fmt.Errorf("failed to cleanup chunks: %w", err)
	}

	s.logger.Info("Upload cleaned up", zap.String("upload_id", uploadID))
	return nil
}

// startDiskCleanup démarre le nettoyage périodique des FICHIERS orphelins (Garbage Collector)
func (s *TrackChunkService) startDiskCleanup() {
	ticker := time.NewTicker(s.cleanupInterval)
	defer ticker.Stop()

	for range ticker.C {
		s.CleanupOrphanedChunks(context.Background())
	}
}

// CleanupOrphanedChunks scan le disque et supprime les dossiers qui n'ont pas bougé depuis maxUploadAge
// Ceci agit comme un Garbage Collector pour les fichiers orphelins
func (s *TrackChunkService) CleanupOrphanedChunks(ctx context.Context) {
	s.logger.Debug("Starting orphaned chunks cleanup")

	entries, err := os.ReadDir(s.chunksDir)
	if err != nil {
		s.logger.Error("Failed to read chunks directory", zap.Error(err))
		return
	}

	now := time.Now()
	deletedCount := 0

	for _, entry := range entries {
		if !entry.IsDir() {
			continue
		}

		info, err := entry.Info()
		if err != nil {
			continue
		}

		// Si le dossier est plus vieux que maxUploadAge
		if now.Sub(info.ModTime()) > s.maxUploadAge {
			// On vérifie s'il existe dans Redis (au cas où Redis a été flushé mais pas les fichiers, ou TTL mismatch)
			// Si Redis n'a plus l'info, on considère que c'est orphelin
			uploadID := entry.Name()
			_, err := s.store.GetState(ctx, uploadID)

			if err != nil {
				// Upload not in Redis (or error), assume safe to delete if older than 24h
				path := filepath.Join(s.chunksDir, uploadID)
				if err := os.RemoveAll(path); err != nil {
					s.logger.Warn("Failed to delete orphaned chunk folder", zap.String("path", path), zap.Error(err))
				} else {
					deletedCount++
					s.logger.Info("Deleted orphaned upload folder", zap.String("upload_id", uploadID))
				}
			}
		}
	}

	if deletedCount > 0 {
		s.logger.Info("Cleanup completed", zap.Int("deleted_count", deletedCount))
	}
}
