package minio

import (
	"context"
	"fmt"
	"io"
	"mime"
	"net/url"
	"path/filepath"
	"strings"
	"time"

	"github.com/minio/minio-go/v7"
)

// Service provides MinIO file storage operations
type Service struct {
	connMgr *ConnectionManager
	config  Config
	logger  Logger
}

// NewService creates a new MinIO service
func NewService(connMgr *ConnectionManager, config Config, logger Logger) *Service {
	return &Service{
		connMgr: connMgr,
		config:  config,
		logger:  logger,
	}
}

// Upload uploads a file to the specified bucket
func (s *Service) Upload(ctx context.Context, bucket, objectName string, reader io.Reader, options UploadOptions) error {
	if !s.connMgr.IsConnected() {
		return ErrNotConnected
	}

	client := s.connMgr.GetClient().(*minio.Client)

	// Set default content type if not provided
	contentType := options.ContentType
	if contentType == "" {
		contentType = mime.TypeByExtension(filepath.Ext(objectName))
		if contentType == "" {
			contentType = "application/octet-stream"
		}
	}

	// Upload the file
	_, err := client.PutObject(ctx, bucket, objectName, reader, -1, minio.PutObjectOptions{
		ContentType:  contentType,
		UserMetadata: options.Metadata,
	})
	if err != nil {
		s.logger.Error("Failed to upload file", map[string]interface{}{
			"bucket":     bucket,
			"objectName": objectName,
			"error":      err.Error(),
		})
		return NewErrorWithCause(ErrCodeUploadFailed, "failed to upload file", err)
	}

	s.logger.Info("File uploaded successfully", map[string]interface{}{
		"bucket":     bucket,
		"objectName": objectName,
		"contentType": contentType,
	})

	return nil
}

// UploadHierarchical uploads a file to hierarchical storage with classification
func (s *Service) UploadHierarchical(ctx context.Context, file io.Reader, filename string, options HierarchicalUploadOptions) (string, error) {
	if !s.connMgr.IsConnected() {
		return "", ErrNotConnected
	}

	// Generate hierarchical path
	objectPath := s.GetDocumentPath(options.Category, options.SubCategory, filename, options.Tags)

	// Ensure bucket exists
	err := s.connMgr.CreateBucket(ctx, s.config.HierarchicalBucket)
	if err != nil {
		return "", fmt.Errorf("failed to create hierarchical bucket: %w", err)
	}

	// Set metadata for classification
	metadata := options.Metadata
	if metadata == nil {
		metadata = make(map[string]string)
	}
	metadata["category"] = options.Category
	metadata["subcategory"] = options.SubCategory
	if len(options.Tags) > 0 {
		metadata["tags"] = strings.Join(options.Tags, ",")
	}
	metadata["original_filename"] = filename
	metadata["uploaded_at"] = fmt.Sprintf("%d", time.Now().Unix())

	uploadOptions := UploadOptions{
		ContentType: options.ContentType,
		Metadata:    metadata,
	}

	err = s.Upload(ctx, s.config.HierarchicalBucket, objectPath, file, uploadOptions)
	if err != nil {
		return "", err
	}

	s.logger.Info("Hierarchical file uploaded successfully", map[string]interface{}{
		"bucket":     s.config.HierarchicalBucket,
		"objectPath": objectPath,
		"category":   options.Category,
		"subCategory": options.SubCategory,
	})

	return objectPath, nil
}

// Download downloads a file from the specified bucket
func (s *Service) Download(ctx context.Context, bucket, objectName string, options DownloadOptions) (io.ReadCloser, error) {
	if !s.connMgr.IsConnected() {
		return nil, ErrNotConnected
	}

	client := s.connMgr.GetClient().(*minio.Client)

	getOptions := minio.GetObjectOptions{}
	if options.Range != nil {
		getOptions.SetRange(options.Range.Start, options.Range.End)
	}

	object, err := client.GetObject(ctx, bucket, objectName, getOptions)
	if err != nil {
		s.logger.Error("Failed to download file", map[string]interface{}{
			"bucket":     bucket,
			"objectName": objectName,
			"error":      err.Error(),
		})
		return nil, NewErrorWithCause(ErrCodeDownloadFailed, "failed to download file", err)
	}

	s.logger.Debug("File download initiated", map[string]interface{}{
		"bucket":     bucket,
		"objectName": objectName,
	})

	return object, nil
}

// Delete deletes a file from the specified bucket
func (s *Service) Delete(ctx context.Context, bucket, objectName string) error {
	if !s.connMgr.IsConnected() {
		return ErrNotConnected
	}

	client := s.connMgr.GetClient().(*minio.Client)

	err := client.RemoveObject(ctx, bucket, objectName, minio.RemoveObjectOptions{})
	if err != nil {
		s.logger.Error("Failed to delete file", map[string]interface{}{
			"bucket":     bucket,
			"objectName": objectName,
			"error":      err.Error(),
		})
		return NewErrorWithCause(ErrCodeDeleteFailed, "failed to delete file", err)
	}

	s.logger.Info("File deleted successfully", map[string]interface{}{
		"bucket":     bucket,
		"objectName": objectName,
	})

	return nil
}

// GetInfo gets information about a file
func (s *Service) GetInfo(ctx context.Context, bucket, objectName string) (*FileInfo, error) {
	if !s.connMgr.IsConnected() {
		return nil, ErrNotConnected
	}

	client := s.connMgr.GetClient().(*minio.Client)

	stat, err := client.StatObject(ctx, bucket, objectName, minio.StatObjectOptions{})
	if err != nil {
		s.logger.Error("Failed to get file info", map[string]interface{}{
			"bucket":     bucket,
			"objectName": objectName,
			"error":      err.Error(),
		})
		return nil, NewErrorWithCause(ErrCodeObjectNotFound, "failed to get file info", err)
	}

	fileInfo := &FileInfo{
		Name:         filepath.Base(objectName),
		Path:         objectName,
		Size:         stat.Size,
		ContentType:  stat.ContentType,
		ETag:         stat.ETag,
		LastModified: stat.LastModified.Unix(),
		Metadata:     stat.UserMetadata,
	}

	return fileInfo, nil
}

// List lists files in a bucket with optional prefix
func (s *Service) List(ctx context.Context, bucket string, options ListOptions) (*ListResult, error) {
	if !s.connMgr.IsConnected() {
		return nil, ErrNotConnected
	}

	client := s.connMgr.GetClient().(*minio.Client)

	listOptions := minio.ListObjectsOptions{
		Prefix:    options.Prefix,
		Recursive: false,
	}

	if options.Delimiter != "" {
		listOptions = minio.ListObjectsOptions{
			Prefix:    options.Prefix,
			Recursive: false,
			UseV1:     true,
		}
	}

	if options.MaxKeys > 0 {
		listOptions.MaxKeys = int(options.MaxKeys)
	}

	objectCh := client.ListObjects(ctx, bucket, listOptions)

	var objects []FileInfo
	var commonPrefixes []string

	for object := range objectCh {
		if object.Err != nil {
			s.logger.Error("Error listing objects", map[string]interface{}{
				"bucket": bucket,
				"prefix": options.Prefix,
				"error":  object.Err.Error(),
			})
			return nil, NewErrorWithCause("LIST_FAILED", "failed to list objects", object.Err)
		}

		if strings.HasSuffix(object.Key, "/") {
			// This is a "directory" (common prefix)
			commonPrefixes = append(commonPrefixes, object.Key)
		} else {
			// This is a file
			fileInfo := FileInfo{
				Name:         filepath.Base(object.Key),
				Path:         object.Key,
				Size:         object.Size,
				ContentType:  "", // Would need to get from StatObject if needed
				ETag:         object.ETag,
				LastModified: object.LastModified.Unix(),
				Metadata:     nil,
			}
			objects = append(objects, fileInfo)
		}
	}

	result := &ListResult{
		Objects:        objects,
		CommonPrefixes: commonPrefixes,
		IsTruncated:    false, // MinIO Go client doesn't provide this info directly
		NextMarker:     "",
	}

	s.logger.Debug("Objects listed successfully", map[string]interface{}{
		"bucket":        bucket,
		"prefix":        options.Prefix,
		"objects_count": len(objects),
		"prefixes_count": len(commonPrefixes),
	})

	return result, nil
}

// Copy copies a file within the same bucket or to another bucket
func (s *Service) Copy(ctx context.Context, srcBucket, srcObject, destBucket, destObject string) error {
	if !s.connMgr.IsConnected() {
		return ErrNotConnected
	}

	client := s.connMgr.GetClient().(*minio.Client)

	src := minio.CopySrcOptions{
		Bucket: srcBucket,
		Object: srcObject,
	}
	dst := minio.CopyDestOptions{
		Bucket: destBucket,
		Object: destObject,
	}

	_, err := client.CopyObject(ctx, dst, src)
	if err != nil {
		s.logger.Error("Failed to copy file", map[string]interface{}{
			"srcBucket":  srcBucket,
			"srcObject":  srcObject,
			"destBucket": destBucket,
			"destObject": destObject,
			"error":      err.Error(),
		})
		return NewErrorWithCause(ErrCodeCopyFailed, "failed to copy file", err)
	}

	s.logger.Info("File copied successfully", map[string]interface{}{
		"srcBucket":  srcBucket,
		"srcObject":  srcObject,
		"destBucket": destBucket,
		"destObject": destObject,
	})

	return nil
}

// Move moves a file (copy + delete)
func (s *Service) Move(ctx context.Context, srcBucket, srcObject, destBucket, destObject string) error {
	err := s.Copy(ctx, srcBucket, srcObject, destBucket, destObject)
	if err != nil {
		return err
	}

	return s.Delete(ctx, srcBucket, srcObject)
}

// Exists checks if a file exists
func (s *Service) Exists(ctx context.Context, bucket, objectName string) (bool, error) {
	_, err := s.GetInfo(ctx, bucket, objectName)
	if err != nil {
		if strings.Contains(err.Error(), "NoSuchKey") || strings.Contains(err.Error(), "not found") {
			return false, nil
		}
		return false, err
	}
	return true, nil
}

// GetPresignedURL generates a presigned URL for temporary access
func (s *Service) GetPresignedURL(ctx context.Context, bucket, objectName string, expirySeconds int64) (string, error) {
	if !s.connMgr.IsConnected() {
		return "", ErrNotConnected
	}

	client := s.connMgr.GetClient().(*minio.Client)

	reqParams := make(url.Values)
	presignedURL, err := client.PresignedGetObject(ctx, bucket, objectName, time.Duration(expirySeconds)*time.Second, reqParams)
	if err != nil {
		s.logger.Error("Failed to generate presigned URL", map[string]interface{}{
			"bucket":     bucket,
			"objectName": objectName,
			"expiry":     expirySeconds,
			"error":      err.Error(),
		})
		return "", NewErrorWithCause(ErrCodePresignFailed, "failed to generate presigned URL", err)
	}

	return presignedURL.String(), nil
}

// UploadDocument uploads a document with hierarchical classification
func (s *Service) UploadDocument(ctx context.Context, file io.Reader, filename string, options HierarchicalUploadOptions) (string, error) {
	return s.UploadHierarchical(ctx, file, filename, options)
}

// ListDocuments lists documents in a category/subcategory
func (s *Service) ListDocuments(ctx context.Context, category, subCategory string, options ListOptions) (*ListResult, error) {
	// Build prefix for hierarchical listing
	prefix := s.buildCategoryPrefix(category, subCategory)
	if prefix != "" && !strings.HasSuffix(prefix, s.config.PathSeparator) {
		prefix += s.config.PathSeparator
	}

	listOptions := ListOptions{
		Prefix:    prefix,
		Delimiter: s.config.PathSeparator,
		MaxKeys:   options.MaxKeys,
		Marker:    options.Marker,
	}

	return s.List(ctx, s.config.HierarchicalBucket, listOptions)
}

// GetDocumentPath generates the hierarchical path for a document
func (s *Service) GetDocumentPath(category, subCategory, filename string, tags []string) string {
	var pathParts []string

	if category != "" {
		pathParts = append(pathParts, category)
	}

	if subCategory != "" {
		pathParts = append(pathParts, subCategory)
	}

	if len(tags) > 0 {
		// Create a subdirectory based on tags
		tagDir := strings.Join(tags, "_")
		if tagDir != "" {
			pathParts = append(pathParts, tagDir)
		}
	}

	// Add timestamp for uniqueness
	timestamp := fmt.Sprintf("%d", time.Now().Unix())
	pathParts = append(pathParts, timestamp)

	// Add filename
	pathParts = append(pathParts, filename)

	return strings.Join(pathParts, s.config.PathSeparator)
}

// SearchDocuments searches documents by tags or metadata (basic implementation)
func (s *Service) SearchDocuments(ctx context.Context, query string, category, subCategory string, options ListOptions) (*ListResult, error) {
	// For now, do a prefix-based search
	// In a real implementation, you might want to use MinIO's search capabilities or index metadata
	prefix := s.buildCategoryPrefix(category, subCategory)

	listOptions := ListOptions{
		Prefix:    prefix,
		Delimiter: s.config.PathSeparator,
		MaxKeys:   options.MaxKeys,
		Marker:    options.Marker,
	}

	result, err := s.List(ctx, s.config.HierarchicalBucket, listOptions)
	if err != nil {
		return nil, err
	}

	// Basic filtering based on query (could be enhanced)
	if query != "" {
		var filteredObjects []FileInfo
		for _, obj := range result.Objects {
			// Check if query matches filename or metadata
			if strings.Contains(strings.ToLower(obj.Name), strings.ToLower(query)) {
				filteredObjects = append(filteredObjects, obj)
			} else {
				// Check metadata
				for _, value := range obj.Metadata {
					if strings.Contains(strings.ToLower(value), strings.ToLower(query)) {
						filteredObjects = append(filteredObjects, obj)
						break
					}
				}
			}
		}
		result.Objects = filteredObjects
	}

	return result, nil
}

// buildCategoryPrefix builds the prefix for category-based searches
func (s *Service) buildCategoryPrefix(category, subCategory string) string {
	var parts []string

	if category != "" {
		parts = append(parts, category)
	}

	if subCategory != "" {
		parts = append(parts, subCategory)
	}

	if len(parts) == 0 {
		return ""
	}

	return strings.Join(parts, s.config.PathSeparator)
}

// validateFileType validates if the file type is allowed
func (s *Service) validateFileType(contentType string) error {
	if len(s.config.AllowedTypes) == 1 && s.config.AllowedTypes[0] == "*" {
		return nil // Allow all types
	}

	for _, allowedType := range s.config.AllowedTypes {
		if allowedType == contentType || strings.HasPrefix(contentType, allowedType) {
			return nil
		}
	}

	return NewError(ErrCodeInvalidFileType, fmt.Sprintf("file type %s is not allowed", contentType))
}

// validateFileSize validates if the file size is within limits
func (s *Service) validateFileSize(size int64) error {
	if size > s.config.MaxFileSize {
		return NewError(ErrCodeFileTooLarge, fmt.Sprintf("file size %d exceeds maximum allowed size %d", size, s.config.MaxFileSize))
	}
	return nil
}
