using System.Linq.Expressions;

using Microsoft.Extensions.Logging;

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

using DomainResults.Common;

using DomainObjects.Abstractions;

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;
    private protected readonly IMongoCollection<T> _collection;

    /// <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,
      string databaseName,
      string collectionName
    ) : base (logger, client, databaseName) {
      _idGenerator = idGenerator;
      _sessionService = sessionService;
      _collection = _database.GetCollection<T>(collectionName);
    }

    #region Insert
    public (Guid?, IDomainResult) Insert(T obj) =>
      InsertAsync(obj).Result;

    public (Guid?, IDomainResult) Insert(T obj, Guid sessionId) =>
      InsertAsync(obj, sessionId).Result;

    public Task<(Guid?, IDomainResult)> InsertAsync(T obj) =>
      InsertAsync(obj, null);

    public Task<(Guid?, IDomainResult)> InsertAsync(T obj, Guid sessionId) =>
      InsertAsync(obj, sessionId);

    private async Task<(Guid?, IDomainResult)> InsertAsync(T obj, Guid? sessionId) {
      try {
        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?>();
      }
    }
    #endregion

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

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

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

    private protected Task<(List<Guid>?, IDomainResult)> InsertManyAsync(List<T> objList, Guid sessionId) =>
      InsertManyAsync(objList, sessionId);

    private async Task<(List<Guid>?, IDomainResult)> InsertManyAsync(List<T> objList, Guid? sessionId) {
      try {
        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>?>();
      }
    }
    #endregion

    #region Get
    private protected (List<T>?, IDomainResult) GetWithPredicate(Expression<Func<T, bool>> predicate) =>
      GetWithPredicate(predicate, 0, 0);

    private protected (List<T>?, IDomainResult) GetWithPredicate(Expression<Func<T, bool>> predicate, int skip, int limit) {
      try {
        var result = _collection
           .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>?>();
      }
    }
    #endregion

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

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

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

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

    private async Task<(Guid?, IDomainResult)> UpdateWithPredicateAsync(T obj, Expression<Func<T, bool>> predicate, Guid? sessionId) {
      try {


        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?>();
      }
    }
    #endregion

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

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

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

    private protected Task<IDomainResult> DeleteWithPredicateAsync(Expression<Func<T, bool>> predicate, Guid sessionId) =>
      DeleteWithPredicateAsync(predicate, sessionId);

    private async Task<IDomainResult> DeleteWithPredicateAsync(Expression<Func<T, bool>> predicate, Guid? sessionId) {
      try {
        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();
      }
    }
    #endregion

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

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

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

    private protected Task<IDomainResult> DeleteManyWithPredicateAsync(Expression<Func<T, bool>> predicate, Guid sessionId) =>
      DeleteManyWithPredicateAsync(predicate, sessionId);

    private async Task<IDomainResult> DeleteManyWithPredicateAsync(Expression<Func<T, bool>> predicate, Guid? sessionId) {
      try {

        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
  }
}
