using BukiVedi.Shared.Entities;
using MongoDB.Bson;
using MongoDB.Driver;
using System.Linq.Expressions;
using ZeroLevel.Services.FileSystem;

namespace BukiVedi.Shared.Services.MongoDB
{
    public class MongoRepository<T>
        : IRepository<T>
        where T : IEntity
    {
        private readonly MongoClient _dbClient;
        private readonly IMongoDatabase _database;
        protected readonly IMongoCollection<T> _collection;
        IMongoCollection<T> IRepository<T>.Collection { get { return _collection; } }

        public MongoRepository(MongoClient client, string dbname, string collectionName)
        {
            _dbClient = client;
            _database = _dbClient.GetDatabase(dbname);
            _collection = GetCollection(collectionName);
        }

        public async Task<T[]> Get(FilterDefinition<T> predicate)
        {
            return (await _collection.FindAsync(predicate))?.ToEnumerable()?.ToArray();
        }

        public async Task<T> GetLast(FilterDefinition<T> predicate, string sortField)
        {
            var builder = Builders<T>.Sort;
            var sort = builder.Descending(sortField);
            var sortedResults = _collection.Find(predicate).Sort(sort);
            var recordWithMaxValue = await sortedResults.FirstOrDefaultAsync();
            return recordWithMaxValue;
        }

        public T GetFirst(FilterDefinition<T> predicate, Expression<Func<T, object>> sortExpr)
        {
            var collection = _collection.Find(predicate);
            if (collection != null && collection.Any())
            {
                return collection.SortBy(sortExpr).FirstOrDefault();
            }
            return default!;
        }
        public async Task<bool> ExistById(string id)
        {
            if (string.IsNullOrWhiteSpace(id)) return false;
            var result = await _collection.CountDocumentsAsync(l => l.Id == id);
            return result > 0;
        }
        public async Task<T[]> GetAll()
        {
            var r = (await _collection.FindAsync(Builders<T>.Filter.Empty))?.ToEnumerable()?.ToArray();
            return r!;
        }

        public async Task<T> GetById(string id)
        {
            return (await _collection.FindAsync(l => l.Id == id)).SingleOrDefault();
        }

        public async Task<bool> Exists(FilterDefinition<T> filter)
        {
            var result = await _collection.CountDocumentsAsync(filter);
            return result >= 1;
        }

        public bool CollectionExists(string collectionName)
        {
            var filter = new BsonDocument("name", collectionName);
            //filter by collection name
            var collections = _database.ListCollections(new ListCollectionsOptions { Filter = filter });
            //check for existence
            return collections.Any();
        }
        private IMongoCollection<T> GetCollectionForType(Type type)
        {
            var collectionName = FSUtils.FileNameCorrection(type.FullName).ToLowerInvariant();
            if (!CollectionExists(collectionName))
            {
                _database.CreateCollection(collectionName);
            }
            var collection = _database.GetCollection<T>(collectionName);
            return collection;
        }

        private IMongoCollection<T> GetCollection(string collectionName)
        {
            if (!CollectionExists(collectionName))
            {
                _database.CreateCollection(collectionName);
            }
            var collection = _database.GetCollection<T>(collectionName);
            return collection;
        }


        public async Task<IEnumerable<TOutput>> Aggregate<TOutput>(PipelineDefinition<T, TOutput> pipeline)
        {
            var result = await _collection.AggregateAsync(pipeline);
            return result.ToEnumerable().ToArray();
        }

        public async Task<long> Count(FilterDefinition<T> filter)
        {
            return await _collection.CountDocumentsAsync(filter);
        }

        #region CUD
        private readonly FindOneAndUpdateOptions<T> _updateOptions = new FindOneAndUpdateOptions<T>() { IsUpsert = false, ReturnDocument = ReturnDocument.After };

        public async Task Drop()
        {
            await _collection.DeleteManyAsync(Builders<T>.Filter.Empty);
        }

        public async Task<bool> TryRemove(FilterDefinition<T> filter)
        {
            var result = await _collection.DeleteManyAsync(filter);
            return result.DeletedCount >= 1;
        }

        public async Task<bool> TryRemove(T obj)
        {
            var result = await _collection.DeleteOneAsync(l => l.Id == obj.Id);
            return result.DeletedCount == 1;
        }

        public async Task<bool> TryRemoveById(string id)
        {
            var result = await _collection.DeleteOneAsync(l => l.Id == id);
            return result.DeletedCount == 1;
        }

        public async Task<T> Write(T record)
        {
            await _collection.InsertOneAsync(record);
            return record;
        }

        public async Task<IEnumerable<T>> WriteMany(IEnumerable<T> records)
        {
            await _collection.InsertManyAsync(records);
            return records;
        }

        public async Task<T> ModifyOne(FilterDefinition<T> filter, UpdateDefinition<T> update)
        {
            return await _collection.FindOneAndUpdateAsync(filter, update, _updateOptions);
        }

        public async Task<UpdateResult> Modify(FilterDefinition<T> filter, UpdateDefinition<T> update)
        {
            return await _collection.UpdateManyAsync(filter, update);
        }

        public async Task<T> ReWrite(T record)
        {
            await _collection.FindOneAndReplaceAsync(Builders<T>.Filter.Eq(r => r.Id, record.Id), record, new FindOneAndReplaceOptions<T, T> { IsUpsert = false });
            return await GetById(record.Id);
        }

        public async Task<T> WriteOrGetExists(T record, FilterDefinition<T> filter)
        {
            var exists = await _collection.FindAsync(filter);
            if (exists != null && exists.Any())
                return exists.First();
            await _collection.InsertOneAsync(record);
            return record;
        }

        public async Task<IEnumerable<T>> Write(IEnumerable<T> records)
        {
            await _collection.InsertManyAsync(records);
            return records;
        }
        #endregion
    }
}
