package services

import (
	"bytes"
	"context"
	"fmt"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/config"
	"github.com/aws/aws-sdk-go-v2/credentials"
	"github.com/aws/aws-sdk-go-v2/feature/s3/manager"
	"github.com/aws/aws-sdk-go-v2/service/s3"
	"go.uber.org/zap"
)

// S3StorageService implémente l'interface S3Service pour le stockage S3-compatible
type S3StorageService struct {
	client    *s3.Client
	uploader  *manager.Uploader
	bucket    string
	region    string
	endpoint  string // Pour MinIO ou autres services S3-compatibles
	logger    *zap.Logger
	urlExpiry time.Duration // Durée de validité des URLs présignées
}

// S3Config contient la configuration pour le service S3
type S3Config struct {
	Bucket    string
	Region    string
	Endpoint  string // Optionnel, pour MinIO ou autres services S3-compatibles
	AccessKey string
	SecretKey string
	URLExpiry time.Duration // Durée de validité des URLs présignées (par défaut 1h)
	Logger    *zap.Logger
}

// NewS3StorageService crée un nouveau service de stockage S3
func NewS3StorageService(cfg S3Config) (*S3StorageService, error) {
	if cfg.Bucket == "" {
		return nil, fmt.Errorf("S3 bucket name is required")
	}
	if cfg.Region == "" {
		cfg.Region = "us-east-1" // Par défaut
	}
	if cfg.URLExpiry == 0 {
		cfg.URLExpiry = time.Hour // Par défaut 1 heure
	}
	if cfg.Logger == nil {
		cfg.Logger = zap.NewNop()
	}

	// Configuration AWS SDK
	awsCfg, err := config.LoadDefaultConfig(context.Background(),
		config.WithRegion(cfg.Region),
	)
	if err != nil {
		return nil, fmt.Errorf("failed to load AWS config: %w", err)
	}

	// Si des credentials sont fournis explicitement, les utiliser
	if cfg.AccessKey != "" && cfg.SecretKey != "" {
		awsCfg.Credentials = credentials.NewStaticCredentialsProvider(cfg.AccessKey, cfg.SecretKey, "")
	}

	// Créer le client S3
	s3Client := s3.NewFromConfig(awsCfg, func(o *s3.Options) {
		// Si un endpoint personnalisé est fourni (pour MinIO, etc.), l'utiliser
		if cfg.Endpoint != "" {
			o.BaseEndpoint = aws.String(cfg.Endpoint)
			// Pour MinIO et services S3-compatibles, désactiver le path-style
			o.UsePathStyle = true
		}
	})

	// Créer l'uploader avec gestionnaire de retry
	uploader := manager.NewUploader(s3Client, func(u *manager.Uploader) {
		u.PartSize = 10 * 1024 * 1024 // 10MB par partie
		u.Concurrency = 3             // 3 uploads concurrents
	})

	service := &S3StorageService{
		client:    s3Client,
		uploader:  uploader,
		bucket:    cfg.Bucket,
		region:    cfg.Region,
		endpoint:  cfg.Endpoint,
		logger:    cfg.Logger,
		urlExpiry: cfg.URLExpiry,
	}

	// Vérifier que le bucket existe et est accessible
	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()
	if err := service.verifyBucketAccess(ctx); err != nil {
		return nil, fmt.Errorf("failed to verify bucket access: %w", err)
	}

	return service, nil
}

// verifyBucketAccess vérifie que le bucket est accessible
func (s *S3StorageService) verifyBucketAccess(ctx context.Context) error {
	_, err := s.client.HeadBucket(ctx, &s3.HeadBucketInput{
		Bucket: aws.String(s.bucket),
	})
	if err != nil {
		// Logger un avertissement mais ne pas bloquer
		// En production, on devrait créer le bucket ou vérifier qu'il existe
		s.logger.Warn("Bucket does not exist or is not accessible, will attempt to use on first upload",
			zap.String("bucket", s.bucket),
			zap.Error(err),
		)
		// Ne pas retourner d'erreur pour permettre la création du bucket plus tard
		// ou pour permettre l'utilisation avec des buckets créés dynamiquement
	}
	return nil
}

// UploadFile upload un fichier vers S3 et retourne l'URL publique ou la clé
func (s *S3StorageService) UploadFile(ctx context.Context, data []byte, key string, contentType string) (string, error) {
	if key == "" {
		return "", fmt.Errorf("key cannot be empty")
	}
	if len(data) == 0 {
		return "", fmt.Errorf("data cannot be empty")
	}

	// Déterminer le Content-Type si non fourni
	if contentType == "" {
		contentType = "application/octet-stream"
	}

	// Upload vers S3
	_, err := s.uploader.Upload(ctx, &s3.PutObjectInput{
		Bucket:      aws.String(s.bucket),
		Key:         aws.String(key),
		Body:        bytes.NewReader(data),
		ContentType: aws.String(contentType),
		// Métadonnées optionnelles
		Metadata: map[string]string{
			"uploaded-at": time.Now().UTC().Format(time.RFC3339),
		},
	})
	if err != nil {
		s.logger.Error("Failed to upload file to S3",
			zap.Error(err),
			zap.String("key", key),
			zap.String("bucket", s.bucket),
		)
		return "", fmt.Errorf("failed to upload file to S3: %w", err)
	}

	s.logger.Info("File uploaded successfully to S3",
		zap.String("key", key),
		zap.String("bucket", s.bucket),
		zap.Int("size", len(data)),
	)

	// Retourner la clé (l'URL complète sera générée via GetPresignedURL si nécessaire)
	return key, nil
}

// DeleteFile supprime un fichier de S3
func (s *S3StorageService) DeleteFile(ctx context.Context, key string) error {
	if key == "" {
		return fmt.Errorf("key cannot be empty")
	}

	_, err := s.client.DeleteObject(ctx, &s3.DeleteObjectInput{
		Bucket: aws.String(s.bucket),
		Key:    aws.String(key),
	})
	if err != nil {
		s.logger.Error("Failed to delete file from S3",
			zap.Error(err),
			zap.String("key", key),
			zap.String("bucket", s.bucket),
		)
		return fmt.Errorf("failed to delete file from S3: %w", err)
	}

	s.logger.Info("File deleted successfully from S3",
		zap.String("key", key),
		zap.String("bucket", s.bucket),
	)

	return nil
}

// GetPresignedURL génère une URL présignée pour télécharger un fichier
func (s *S3StorageService) GetPresignedURL(ctx context.Context, key string) (string, error) {
	if key == "" {
		return "", fmt.Errorf("key cannot be empty")
	}

	presignClient := s3.NewPresignClient(s.client)

	request, err := presignClient.PresignGetObject(ctx, &s3.GetObjectInput{
		Bucket: aws.String(s.bucket),
		Key:    aws.String(key),
	}, func(opts *s3.PresignOptions) {
		opts.Expires = s.urlExpiry
	})
	if err != nil {
		s.logger.Error("Failed to generate presigned URL",
			zap.Error(err),
			zap.String("key", key),
			zap.String("bucket", s.bucket),
		)
		return "", fmt.Errorf("failed to generate presigned URL: %w", err)
	}

	return request.URL, nil
}

// GetPublicURL génère une URL publique (si le bucket est public)
func (s *S3StorageService) GetPublicURL(key string) string {
	if s.endpoint != "" {
		// Pour MinIO ou services S3-compatibles avec endpoint personnalisé
		return fmt.Sprintf("%s/%s/%s", s.endpoint, s.bucket, key)
	}
	// Pour AWS S3 standard
	return fmt.Sprintf("https://%s.s3.%s.amazonaws.com/%s", s.bucket, s.region, key)
}

// ListFiles liste les fichiers dans un préfixe donné
func (s *S3StorageService) ListFiles(ctx context.Context, prefix string) ([]string, error) {
	var keys []string

	paginator := s3.NewListObjectsV2Paginator(s.client, &s3.ListObjectsV2Input{
		Bucket: aws.String(s.bucket),
		Prefix: aws.String(prefix),
	})

	for paginator.HasMorePages() {
		output, err := paginator.NextPage(ctx)
		if err != nil {
			return nil, fmt.Errorf("failed to list objects: %w", err)
		}

		for _, obj := range output.Contents {
			if obj.Key != nil {
				keys = append(keys, *obj.Key)
			}
		}
	}

	return keys, nil
}
