using System.Linq.Expressions;

using Microsoft.Extensions.Logging;

using MongoDB.Bson.Serialization;
using MongoDB.Driver;

using DomainResults.Common;

using Core.Abstractions.DomainObjects;

namespace DataProviders.Abstractions {

  /// <summary>
  /// 
  /// </summary>
  /// <typeparam name="T"></typeparam>
  public abstract class CollectionDataProviderBase<T> : DataProviderBase<CollectionDataProviderBase<T>> where T : DomainObjectDocumentBase<T> {

    private protected readonly IIdGenerator _idGenerator;
    private protected readonly ISessionService _sessionService;

    /// <summary>
    /// Main constructor
    /// </summary>
    /// <param name="logger"></param>
    /// <param name="client"></param>
    /// <param name="idGenerator"></param>
    /// <param name="dataProviderUtils"></param>
    public CollectionDataProviderBase(
      ILogger<CollectionDataProviderBase<T>> logger,
      IMongoClient client,
      IIdGenerator idGenerator,
      ISessionService sessionService
    ) : base (logger, client){
      _idGenerator = idGenerator;
      _sessionService = sessionService;
    }

    #region Insert
    private protected (Guid?, IDomainResult) Insert(T obj, string collectionName) =>
      InsertAsync(obj, collectionName).Result;

    private protected (Guid?, IDomainResult) Insert(T obj, string collectionName, Guid sessionId) =>
      InsertAsync(obj, collectionName, sessionId).Result;

    private protected Task<(Guid?, IDomainResult)> InsertAsync(T obj, string collectionName) =>
      InsertAsyncCore(obj, collectionName, null);

    private protected Task<(Guid?, IDomainResult)> InsertAsync(T obj, string collectionName, Guid sessionId) =>
      InsertAsyncCore(obj, collectionName, sessionId);
    #endregion

    #region InsertMany
    private protected (List<Guid>?, IDomainResult) InsertMany(List<T> objList, string collectionName) =>
      InsertManyAsync(objList, collectionName).Result;

    private protected (List<Guid>?, IDomainResult) InsertMany(List<T> objList, string collectionName, Guid sessionId) =>
      InsertManyAsync(objList, collectionName, sessionId).Result;

    private protected Task<(List<Guid>?, IDomainResult)> InsertManyAsync(List<T> objList, string collectionName) =>
      InsertManyAsyncCore(objList, collectionName, null);

    private protected Task<(List<Guid>?, IDomainResult)> InsertManyAsync(List<T> objList, string collectionName, Guid sessionId) =>
      InsertManyAsyncCore(objList, collectionName, sessionId);
    #endregion

    #region Get
    private protected (List<T>?, IDomainResult) GetWithPredicate(Expression<Func<T, bool>> predicate, string collectionName) =>
      GetWithPredicateCore(predicate, 0, 0, collectionName);
    private protected (List<T>?, IDomainResult) GetWithPredicate(Expression<Func<T, bool>> predicate, int skip, int limit, string collectionName) =>
      GetWithPredicateCore(predicate, skip, limit, collectionName);
    #endregion

    #region Update
    private protected (Guid?, IDomainResult) UpdateWithPredicate(T obj, Expression<Func<T, bool>> predicate, string collectionName) =>
      UpdateWithPredicateAsync(obj, predicate, collectionName).Result;

    private protected (Guid?, IDomainResult) UpdateWithPredicate(T obj, Expression<Func<T, bool>> predicate, string collectionName, Guid sessionId) =>
      UpdateWithPredicateAsync(obj, predicate, collectionName, sessionId).Result;

    private protected Task<(Guid?, IDomainResult)> UpdateWithPredicateAsync(T obj, Expression<Func<T, bool>> predicate, string collectionName) =>
      UpdateWithPredicateAsyncCore(obj, predicate, collectionName, null);

    private protected Task<(Guid?, IDomainResult)> UpdateWithPredicateAsync(T obj, Expression<Func<T, bool>> predicate, string collectionName, Guid sessionId) =>
      UpdateWithPredicateAsyncCore(obj, predicate, collectionName, sessionId);
    #endregion

    #region Delete
    private protected IDomainResult DeleteWithPredicate(Expression<Func<T, bool>> predicate, string collectionName) =>
      DeleteWithPredicateAsync(predicate, collectionName).Result;

    private protected IDomainResult DeleteWithPredicate(Expression<Func<T, bool>> predicate, string collectionName, Guid sessionId) =>
      DeleteWithPredicateAsync(predicate, collectionName, sessionId).Result;

    private protected Task<IDomainResult> DeleteWithPredicateAsync(Expression<Func<T, bool>> predicate, string collectionName) =>
      DeleteWithPredicateAsyncCore(predicate, collectionName, null);

    private protected Task<IDomainResult> DeleteWithPredicateAsync(Expression<Func<T, bool>> predicate, string collectionName, Guid sessionId) =>
      DeleteWithPredicateAsyncCore(predicate, collectionName, sessionId);
    #endregion

    #region DeleteMany
    private protected IDomainResult DeleteManyWithPredicate(Expression<Func<T, bool>> predicate, string collectionName) =>
      DeleteManyWithPredicateAsync(predicate, collectionName).Result;

    private protected IDomainResult DeleteManyWithPredicate(Expression<Func<T, bool>> predicate, string collectionName, Guid sessionId) =>
      DeleteManyWithPredicateAsync(predicate, collectionName, sessionId).Result;

    private protected Task<IDomainResult> DeleteManyWithPredicateAsync(Expression<Func<T, bool>> predicate, string collectionName) =>
      DeleteManyWithPredicateAsyncCore(predicate, collectionName, null);

    private protected Task<IDomainResult> DeleteManyWithPredicateAsync(Expression<Func<T, bool>> predicate, string collectionName, Guid sessionId) =>
      DeleteManyWithPredicateAsyncCore(predicate, collectionName, sessionId);
    #endregion

    #region Core methods
    private async protected Task<(Guid?, IDomainResult)> InsertAsyncCore(T obj, string collectionName, Guid? sessionId) {
      try {
        var collection = _client.GetDatabase(_databaseName).GetCollection<T>(collectionName);

        if (sessionId != null)
          await collection.InsertOneAsync(_sessionService.GetSession(sessionId.Value), obj);
        else
          collection.InsertOne(obj);

        return IDomainResult.Success(obj.Id);
      }
      catch (Exception ex) {
        _logger.LogError(ex, "Data provider error");
        return IDomainResult.Failed<Guid?>();
      }
    }

    private async Task<(List<Guid>?, IDomainResult)> InsertManyAsyncCore(List<T> objList, string collectionName, Guid? sessionId) {
      try {
        var collection = _client.GetDatabase(_databaseName).GetCollection<T>(collectionName);

        if (sessionId != null)
          await collection.InsertManyAsync(_sessionService.GetSession(sessionId.Value), objList);
        else
          collection.InsertMany(objList);

        return IDomainResult.Success(objList.Select(x => x.Id).ToList());
      }
      catch (Exception ex) {
        _logger.LogError(ex, "Data provider error");
        return IDomainResult.Failed<List<Guid>?>();
      }
    }

    private (List<T>?, IDomainResult) GetWithPredicateCore(Expression<Func<T, bool>> predicate, int skip, int limit, string collectionName) {
      try {
        var result = _client.GetDatabase(_databaseName).GetCollection<T>(collectionName)
           .Find(predicate).Skip(skip).Limit(limit).ToList();

        return result != null && result.Count > 0
          ? IDomainResult.Success(result)
          : IDomainResult.NotFound<List<T>?>();
      }
      catch (Exception ex) {
        _logger.LogError(ex, "Data provider error");
        return IDomainResult.Failed<List<T>?>();
      }
    }

    private async Task<(Guid?, IDomainResult)> UpdateWithPredicateAsyncCore(T obj, Expression<Func<T, bool>> predicate, string collectionName, Guid? sessionId) {
      try {
       
        var collection = _client.GetDatabase(_databaseName).GetCollection<T>(collectionName);

        if (sessionId != null)
          await collection.ReplaceOneAsync(_sessionService.GetSession(sessionId.Value), predicate, obj);
        else
          await collection.ReplaceOneAsync(predicate, obj);

        return IDomainResult.Success(obj.Id);
      }
      catch (Exception ex) {
        _logger.LogError(ex, "Data provider error");
        return IDomainResult.Failed<Guid?>();
      }
    }

    private async Task<IDomainResult> DeleteWithPredicateAsyncCore(Expression<Func<T, bool>> predicate, string collectionName, Guid? sessionId) {
      try {
        var collection = _client.GetDatabase(_databaseName).GetCollection<T>(collectionName);

        if (sessionId != null)
          await collection.DeleteOneAsync<T>(_sessionService.GetSession(sessionId.Value), predicate);
        else
          await collection.DeleteOneAsync<T>(predicate);

        return IDomainResult.Success();
      }
      catch (Exception ex) {
        _logger.LogError(ex, "Data provider error");
        return IDomainResult.Failed();
      }
    }

    private async Task<IDomainResult> DeleteManyWithPredicateAsyncCore(Expression<Func<T, bool>> predicate, string collectionName, Guid? sessionId) {
      try {
        var collection = _client.GetDatabase(_databaseName).GetCollection<T>(collectionName);

        if (sessionId != null)
          await collection.DeleteManyAsync(_sessionService.GetSession(sessionId.Value), predicate);
        else
          await collection.DeleteManyAsync(predicate);

        return IDomainResult.Success();
      }
      catch (Exception ex) {
        _logger.LogError(ex, "Data provider error");
        return IDomainResult.Failed();
      }
    }
    #endregion
  }
}
