package filestore

import (
	"bytes"
	"context"
	"io"
	"time"

	"tm/pkg/logger"
	"tm/pkg/mongo"

	"go.mongodb.org/mongo-driver/v2/bson"
	mongopkg "go.mongodb.org/mongo-driver/v2/mongo"
	"go.mongodb.org/mongo-driver/v2/mongo/options"
)

// GridFSService implements FileStoreService using MongoDB GridFS
type GridFSService struct {
	database mongopkg.Database
	logger   logger.Logger
}

// NewGridFSService creates a new GridFS file store service
func NewGridFSService(mongoManager *mongo.ConnectionManager, logger logger.Logger) (*GridFSService, error) {
	if !mongoManager.IsConnected() {
		return nil, NewError(ErrCodeUploadFailed, "MongoDB connection not available")
	}

	database := mongoManager.GetDatabase()
	if database == nil {
		return nil, NewError(ErrCodeUploadFailed, "MongoDB database not available")
	}

	logger.Info("GridFS service initialized successfully", map[string]interface{}{
		"storage_type": "MongoDB GridFS",
	})

	return &GridFSService{
		database: *database,
		logger:   logger,
	}, nil
}

// Upload stores a file in GridFS
func (s *GridFSService) Upload(fileID, filename string, reader io.Reader, options UploadOptions) (string, error) {
	// Validate input
	if filename == "" {
		return "", ErrInvalidInput
	}

	if options.ContentType == "" {
		options.ContentType = "application/octet-stream"
	}

	// Check file size if max size is set
	if options.MaxSize > 0 {
		// Read into buffer to check size
		data, err := io.ReadAll(reader)
		if err != nil {
			s.logger.Error("Failed to read file for size check", map[string]interface{}{
				"filename": filename,
				"error":    err.Error(),
			})
			return "", NewErrorWithCause(ErrCodeUploadFailed, "failed to read file", err)
		}

		if int64(len(data)) > options.MaxSize {
			s.logger.Warn("File exceeds maximum size", map[string]interface{}{
				"filename": filename,
				"size":     len(data),
				"max_size": options.MaxSize,
			})
			return "", ErrFileTooLarge
		}

		reader = bytes.NewReader(data)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Create files and chunks collections
	filesCollection := s.database.Collection("fs.files")
	chunksCollection := s.database.Collection("fs.chunks")

	// Generate file ID if not provided
	var objectID bson.ObjectID
	var err error
	if fileID != "" {
		objectID, err = bson.ObjectIDFromHex(fileID)
		if err != nil {
			s.logger.Error("Invalid file ID format", map[string]interface{}{
				"file_id": fileID,
				"error":   err.Error(),
			})
			return "", NewErrorWithCause(ErrCodeInvalidInput, "invalid file ID format", err)
		}
	} else {
		objectID = bson.NewObjectID()
	}

	// Read file data
	fileData, err := io.ReadAll(reader)
	if err != nil {
		s.logger.Error("Failed to read file data", map[string]interface{}{
			"filename": filename,
			"error":    err.Error(),
		})
		return "", NewErrorWithCause(ErrCodeUploadFailed, "failed to read file", err)
	}

	// Prepare metadata
	metadata := bson.M{
		"uploaded_at":  time.Now().Unix(),
		"content_type": options.ContentType,
	}

	if options.UploadedBy != "" {
		metadata["uploaded_by"] = options.UploadedBy
	}
	if options.Category != "" {
		metadata["category"] = options.Category
	}
	if len(options.Tags) > 0 {
		metadata["tags"] = options.Tags
	}
	if options.Description != "" {
		metadata["description"] = options.Description
	}
	if len(options.Extra) > 0 {
		metadata["extra"] = options.Extra
	}

	// Split file into chunks (255 KB default chunk size)
	chunkSize := 255 * 1024
	numChunks := (len(fileData) + chunkSize - 1) / chunkSize

	// Insert chunks
	for i := 0; i < numChunks; i++ {
		start := i * chunkSize
		end := start + chunkSize
		if end > len(fileData) {
			end = len(fileData)
		}

		chunk := bson.M{
			"files_id": objectID,
			"n":        int32(i),
			"data":     fileData[start:end],
		}

		_, err := chunksCollection.InsertOne(ctx, chunk)
		if err != nil {
			s.logger.Error("Failed to insert chunk", map[string]interface{}{
				"filename": filename,
				"chunk":    i,
				"error":    err.Error(),
			})
			return "", NewErrorWithCause(ErrCodeUploadFailed, "failed to store file chunks", err)
		}
	}

	// Insert file record
	fileRecord := bson.M{
		"_id":        objectID,
		"filename":   filename,
		"uploadDate": time.Now(),
		"chunkSize":  int32(chunkSize),
		"length":     int32(len(fileData)),
		"md5":        "", // MongoDB would calculate this, we can leave empty
		"metadata":   metadata,
	}

	_, err = filesCollection.InsertOne(ctx, fileRecord)
	if err != nil {
		s.logger.Error("Failed to insert file record", map[string]interface{}{
			"filename": filename,
			"error":    err.Error(),
		})
		return "", NewErrorWithCause(ErrCodeUploadFailed, "failed to upload file", err)
	}

	uploadedFileID := objectID.Hex()
	s.logger.Info("File uploaded successfully to GridFS", map[string]interface{}{
		"file_id":      uploadedFileID,
		"filename":     filename,
		"content_type": options.ContentType,
		"uploaded_by":  options.UploadedBy,
		"category":     options.Category,
	})

	return uploadedFileID, nil
}

// Download retrieves a file from GridFS
func (s *GridFSService) Download(fileID string) (io.ReadCloser, *FileInfo, error) {
	if fileID == "" {
		return nil, nil, ErrInvalidInput
	}

	objectID, err := bson.ObjectIDFromHex(fileID)
	if err != nil {
		s.logger.Error("Invalid file ID format", map[string]interface{}{
			"file_id": fileID,
			"error":   err.Error(),
		})
		return nil, nil, NewErrorWithCause(ErrCodeInvalidInput, "invalid file ID format", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Get file info first
	fileInfo, err := s.GetFileInfo(fileID)
	if err != nil {
		return nil, nil, err
	}

	// Get all chunks for this file
	chunksCollection := s.database.Collection("fs.chunks")
	findOpts := options.Find().SetSort(bson.D{{Key: "n", Value: 1}})
	cursor, err := chunksCollection.Find(ctx, bson.M{"files_id": objectID}, findOpts)
	if err != nil {
		s.logger.Error("Failed to find chunks", map[string]interface{}{
			"file_id": fileID,
			"error":   err.Error(),
		})
		return nil, nil, NewErrorWithCause(ErrCodeDownloadFailed, "failed to download file", err)
	}
	defer cursor.Close(ctx)

	// Read all chunks and assemble file
	var chunks []bson.M
	if err := cursor.All(ctx, &chunks); err != nil {
		s.logger.Error("Failed to read chunks", map[string]interface{}{
			"file_id": fileID,
			"error":   err.Error(),
		})
		return nil, nil, NewErrorWithCause(ErrCodeDownloadFailed, "failed to read file data", err)
	}

	// Assemble file from chunks
	var fileData bytes.Buffer
	for _, chunk := range chunks {
		data, ok := chunk["data"].(bson.Binary)
		if !ok {
			return nil, nil, NewError(ErrCodeDownloadFailed, "invalid chunk data format")
		}
		fileData.Write(data.Data)
	}

	s.logger.Info("File downloaded successfully from GridFS", map[string]interface{}{
		"file_id":  fileID,
		"filename": fileInfo.Filename,
	})

	return io.NopCloser(&fileData), fileInfo, nil
}

// Delete removes a file from GridFS
func (s *GridFSService) Delete(fileID string) error {
	if fileID == "" {
		return ErrInvalidInput
	}

	objectID, err := bson.ObjectIDFromHex(fileID)
	if err != nil {
		s.logger.Error("Invalid file ID format", map[string]interface{}{
			"file_id": fileID,
			"error":   err.Error(),
		})
		return NewErrorWithCause(ErrCodeInvalidInput, "invalid file ID format", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Delete file record
	filesCollection := s.database.Collection("fs.files")
	_, err = filesCollection.DeleteOne(ctx, bson.M{"_id": objectID})
	if err != nil {
		s.logger.Error("Failed to delete file record", map[string]interface{}{
			"file_id": fileID,
			"error":   err.Error(),
		})
		return NewErrorWithCause(ErrCodeDeleteFailed, "failed to delete file", err)
	}

	// Delete associated chunks
	chunksCollection := s.database.Collection("fs.chunks")
	_, err = chunksCollection.DeleteMany(ctx, bson.M{"files_id": objectID})
	if err != nil {
		s.logger.Error("Failed to delete file chunks", map[string]interface{}{
			"file_id": fileID,
			"error":   err.Error(),
		})
		return NewErrorWithCause(ErrCodeDeleteFailed, "failed to delete file chunks", err)
	}

	s.logger.Info("File deleted successfully from GridFS", map[string]interface{}{
		"file_id": fileID,
	})

	return nil
}

// GetFileInfo retrieves metadata for a file
func (s *GridFSService) GetFileInfo(fileID string) (*FileInfo, error) {
	if fileID == "" {
		return nil, ErrInvalidInput
	}

	objectID, err := bson.ObjectIDFromHex(fileID)
	if err != nil {
		s.logger.Error("Invalid file ID format", map[string]interface{}{
			"file_id": fileID,
			"error":   err.Error(),
		})
		return nil, NewErrorWithCause(ErrCodeInvalidInput, "invalid file ID format", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
	defer cancel()

	// Get file record from fs.files collection
	filesCollection := s.database.Collection("fs.files")
	var fileRecord bson.M
	err = filesCollection.FindOne(ctx, bson.M{"_id": objectID}).Decode(&fileRecord)
	if err != nil {
		s.logger.Error("Failed to find file", map[string]interface{}{
			"file_id": fileID,
			"error":   err.Error(),
		})
		return nil, NewErrorWithCause(ErrCodeNotFound, "file not found", err)
	}

	// Extract upload date
	var uploadDate time.Time
	if ud, ok := fileRecord["uploadDate"].(time.Time); ok {
		uploadDate = ud
	}

	// Extract metadata
	var metadata map[string]interface{}
	if md, ok := fileRecord["metadata"].(bson.M); ok {
		metadata = md
	}

	fileInfo := &FileInfo{
		FileID:      fileID,
		Filename:    fileRecord["filename"].(string),
		ContentType: "application/octet-stream",
		Size:        int64(fileRecord["length"].(int32)),
		UploadedAt:  uploadDate,
		Metadata:    metadata,
	}

	// Extract metadata fields if they exist
	if metadata != nil {
		if ct, ok := metadata["content_type"].(string); ok {
			fileInfo.ContentType = ct
		}
		if ub, ok := metadata["uploaded_by"].(string); ok {
			fileInfo.UploadedBy = ub
		}
		if cat, ok := metadata["category"].(string); ok {
			fileInfo.Category = cat
		}
		if tags, ok := metadata["tags"].([]interface{}); ok {
			fileInfo.Tags = make([]string, len(tags))
			for i, tag := range tags {
				if tagStr, ok := tag.(string); ok {
					fileInfo.Tags[i] = tagStr
				}
			}
		}
		if desc, ok := metadata["description"].(string); ok {
			fileInfo.Description = desc
		}
		if extra, ok := metadata["extra"].(bson.M); ok {
			fileInfo.Extra = make(map[string]string)
			for k, v := range extra {
				if vs, ok := v.(string); ok {
					fileInfo.Extra[k] = vs
				}
			}
		}
	}

	return fileInfo, nil
}

// ListFiles lists files by category with optional filtering
func (s *GridFSService) ListFiles(category string, tags []string, limit int64, skip int64) ([]*FileInfo, int64, error) {
	if limit <= 0 {
		limit = 50
	}
	if limit > 1000 {
		limit = 1000
	}

	ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	// Build filter
	filter := bson.M{}
	if category != "" {
		filter["metadata.category"] = category
	}
	if len(tags) > 0 {
		filter["metadata.tags"] = bson.M{"$in": tags}
	}

	// Find files
	filesCollection := s.database.Collection("fs.files")

	// Count total
	total, err := filesCollection.CountDocuments(ctx, filter)
	if err != nil {
		s.logger.Error("Failed to count files", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, 0, NewErrorWithCause(ErrCodeDownloadFailed, "failed to count files", err)
	}

	// Find and list
	opts := options.Find().SetSkip(skip).SetLimit(limit)

	cursor, err := filesCollection.Find(ctx, filter, opts)
	if err != nil {
		s.logger.Error("Failed to list files", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, 0, NewErrorWithCause(ErrCodeDownloadFailed, "failed to list files", err)
	}
	defer cursor.Close(ctx)

	var fileRecords []bson.M
	if err := cursor.All(ctx, &fileRecords); err != nil {
		s.logger.Error("Failed to decode files", map[string]interface{}{
			"error": err.Error(),
		})
		return nil, 0, NewErrorWithCause(ErrCodeDownloadFailed, "failed to decode files", err)
	}

	// Convert to FileInfo
	var files []*FileInfo
	for _, record := range fileRecords {
		fileID := record["_id"].(bson.ObjectID).Hex()

		var uploadDate time.Time
		if ud, ok := record["uploadDate"].(time.Time); ok {
			uploadDate = ud
		}

		var metadata map[string]interface{}
		if md, ok := record["metadata"].(bson.M); ok {
			metadata = md
		}

		fileInfo := &FileInfo{
			FileID:      fileID,
			Filename:    record["filename"].(string),
			ContentType: "application/octet-stream",
			Size:        int64(record["length"].(int32)),
			UploadedAt:  uploadDate,
			Metadata:    metadata,
		}

		// Extract metadata fields
		if metadata != nil {
			if ct, ok := metadata["content_type"].(string); ok {
				fileInfo.ContentType = ct
			}
			if ub, ok := metadata["uploaded_by"].(string); ok {
				fileInfo.UploadedBy = ub
			}
			if cat, ok := metadata["category"].(string); ok {
				fileInfo.Category = cat
			}
			if tags, ok := metadata["tags"].([]interface{}); ok {
				fileInfo.Tags = make([]string, len(tags))
				for i, tag := range tags {
					if tagStr, ok := tag.(string); ok {
						fileInfo.Tags[i] = tagStr
					}
				}
			}
		}

		files = append(files, fileInfo)
	}

	return files, total, nil
}

// Exists checks if a file exists
func (s *GridFSService) Exists(fileID string) (bool, error) {
	if fileID == "" {
		return false, ErrInvalidInput
	}

	_, err := s.GetFileInfo(fileID)
	if err != nil {
		if IsNotFound(err) {
			return false, nil
		}
		return false, err
	}

	return true, nil
}
