using Microsoft.Extensions.Logging;

using MongoDB.Bson;
using MongoDB.Driver;
using MongoDB.Driver.GridFS;

using DomainResults.Common;

namespace DataProviders.Abstractions {

  /// <summary>
  /// 
  /// </summary>
  /// <typeparam name="T"></typeparam>
  public abstract class BucketDataProviderBase : DataProviderBase<BucketDataProviderBase> {
 
    /// <summary>
    /// 
    /// </summary>
    /// <param name="logger"></param>
    /// <param name="client"></param>
    public BucketDataProviderBase(
      ILogger<BucketDataProviderBase> logger,
      IMongoClient client
    ) : base (logger, client){ }

    #region Upload
    private protected (Guid?, IDomainResult) Upload(Guid siteId, Guid userId, BucketFile file, string bucketName) =>
      UploadAsync(siteId, userId, file, bucketName).Result;

    private protected Task<(Guid?, IDomainResult)> UploadAsync(Guid siteId, Guid userId, BucketFile file, string bucketName) =>
      UploadAsyncCore(siteId, userId, file, bucketName);
    #endregion

    #region Upload many
    private protected (List<Guid>?, IDomainResult) UploadMany(Guid siteId, Guid userId, List<BucketFile> files, string bucketName) =>
      UploadAsync(siteId, userId, files, bucketName).Result;

    private protected Task<(List<Guid>?, IDomainResult)> UploadAsync(Guid siteId, Guid userId, List<BucketFile> files, string bucketName) =>
      UploadManyAsyncCore(siteId, userId, files, bucketName);
    #endregion

    #region Download
    private protected (List<BucketFile>?, IDomainResult) Download(FilterDefinition<GridFSFileInfo> filter, string bucketName) =>
      DownloadAsync(filter, bucketName).Result;

    private protected Task<(List<BucketFile>?, IDomainResult)> DownloadAsync(FilterDefinition<GridFSFileInfo> filter, string bucketName) =>
      DownloadAsyncCore(filter, bucketName);
    #endregion

    #region Delete
    private protected IDomainResult DeleteOne(Guid siteId, Guid userId, Guid fileId, string bucketName) =>
      DeleteOneAsync(siteId, userId, fileId, bucketName).Result;

    private protected Task<IDomainResult> DeleteOneAsync(Guid siteId, Guid userId, Guid fileId, string bucketName) =>
      DeleteOneAsyncCore(siteId, userId, fileId, bucketName);
    #endregion

    #region Delete many
    private protected IDomainResult DeleteMany(Guid siteId, Guid userId, string bucketName) =>
      DeleteManyAsync(siteId, userId, bucketName).Result;

    private protected Task<IDomainResult> DeleteManyAsync(Guid siteId, Guid userId, string bucketName) =>
      DeleteManyAsyncCore(siteId, userId, bucketName);
    #endregion

    #region Core methods
    private GridFSBucket CreateBucket(string bucketName) => new GridFSBucket(_client.GetDatabase(_databaseName), new GridFSBucketOptions {
      BucketName = bucketName,
      ChunkSizeBytes = 1048576, // 1MB
      WriteConcern = WriteConcern.WMajority,
      ReadPreference = ReadPreference.Secondary
    });

    private async Task<(Guid?, IDomainResult)> UploadAsyncCore(Guid siteId, Guid userId, BucketFile file, string bucketName) {
      var (list, result) = await UploadManyAsyncCore(siteId, userId, new List<BucketFile> { file }, bucketName);

      if (!result.IsSuccess || list == null)
        return (null, result);

      return (list.First(), result);
    }
 
    public async Task<(List<Guid>?, IDomainResult)> UploadManyAsyncCore(Guid siteId, Guid userId, List<BucketFile> files, string bucketName) {
      var options = new GridFSUploadOptions {
        ChunkSizeBytes = 64512, // 63KB
      };

      try {
        var bucket = CreateBucket(bucketName);

        var result = new List<Guid>();
        foreach (var file in files) {

          options.Metadata = new BsonDocument {
            { "siteId", $"{siteId}"},
            { "userId", $"{userId}"},
            { "fileName", file.Name },
            { "contentType", file.ContentType },
            { "private", true }
          };

          var fileId = Guid.NewGuid();
          await bucket.UploadFromBytesAsync($"{fileId}", file.Bytes, options);
          result.Add(fileId);
        }

        return result.Count > 0
          ? IDomainResult.Success(result)
          : IDomainResult.Failed<List<Guid>?>();
      }
      catch (Exception ex) {
        _logger.LogError(ex, "Bucket data provider error");
        return IDomainResult.Failed<List<Guid>?>();
      }
    }

    private async Task<(List<BucketFile>?, IDomainResult)> DownloadAsyncCore(FilterDefinition<GridFSFileInfo> filter, string bucketName) {
      try {
        var sort = Builders<GridFSFileInfo>.Sort.Descending(x => x.UploadDateTime);

        var bucket = CreateBucket(bucketName);

        using var cursor = await bucket.FindAsync(filter, new GridFSFindOptions {
          Sort = sort,
        });

        var result = new List<BucketFile>();

        // fileInfo either has the matching file information or is null
        foreach (var fileInfo in await cursor.ToListAsync()) {
          if (fileInfo == null)
            return IDomainResult.NotFound<List<BucketFile>?>();

          var fileName = fileInfo.Metadata["fileName"].ToString() ?? "";
          if (fileName == null)
            return IDomainResult.Failed<List<BucketFile>?>();

          var contentType = fileInfo.Metadata["contentType"].ToString() ?? "";
          if (contentType == null)
            return IDomainResult.Failed<List<BucketFile>?>();

          var bytes = await bucket.DownloadAsBytesByNameAsync($"{fileInfo.Filename}", new GridFSDownloadByNameOptions {
            Revision = -1
          });

          if (bytes == null)
            return IDomainResult.Failed<List<BucketFile>?>();

          result.Add(new BucketFile(fileName, bytes, contentType));
        }

        return result.Count > 0
          ? IDomainResult.Success(result)
          : IDomainResult.NotFound<List<BucketFile>?>();
       
      }
      catch (Exception ex) {
        _logger.LogError(ex, "Bucket data provider error");
        return IDomainResult.Failed<List<BucketFile>?>();
      }
    }

    private Task<IDomainResult> DeleteOneAsyncCore(Guid siteId, Guid userId, Guid fileId, string bucketName) =>
      DeleteAsyncCore(siteId, userId, Builders<GridFSFileInfo>.Filter.And(
        Builders<GridFSFileInfo>.Filter.Eq(x => x.Metadata["siteId"], $"{siteId}"),
        Builders<GridFSFileInfo>.Filter.Eq(x => x.Metadata["userId"], $"{userId}"),
        Builders<GridFSFileInfo>.Filter.Eq(x => x.Filename, $"{fileId}")
      ), bucketName);

    private Task<IDomainResult> DeleteManyAsyncCore(Guid siteId, Guid userId, string bucketName) =>
      DeleteAsyncCore(siteId, userId, Builders<GridFSFileInfo>.Filter.And(
        Builders<GridFSFileInfo>.Filter.Eq(x => x.Metadata["siteId"], $"{siteId}"),
        Builders<GridFSFileInfo>.Filter.Eq(x => x.Metadata["userId"], $"{userId}")
      ), bucketName);

    private async Task<IDomainResult> DeleteAsyncCore(Guid siteId, Guid userId, FilterDefinition<GridFSFileInfo> filter, string bucketName) {
      try {
        var bucket = CreateBucket(bucketName);

        using var cursor = await bucket.FindAsync(filter);

        var count = 0;
        foreach (var fileInfo in await cursor.ToListAsync()) {
          await bucket.DeleteAsync(fileInfo.Id);
          count++;
        }

        return count > 0
          ? IDomainResult.Success()
          : IDomainResult.NotFound();
      }
      catch (Exception ex) {
        _logger.LogError(ex, "Bucket data provider error");
        return IDomainResult.Failed();
      }
    }
    #endregion
  }
}
