using adas_core.Application.Repositories.Interfaces;
using adas_core.Domain.Models.AppSettings;
using adas_core.Infrastructure.Utils;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using Serilog;

namespace adas_core.Infrastructure.Repositories;

/// <summary>
/// Abstract base repository providing common MongoDB persistence operations for a given document type <typeparamref name="T"/>.
/// Concrete repositories must implement <see cref="GetCollectionName"/> to identify the target collection,
/// and may override <see cref="CreateIndexes"/>, <see cref="InsertInitialLoad"/>, and <see cref="InsertOneAsync(T)"/>
/// to customize schema setup, seeding, and insertion behavior.
/// </summary>
/// <typeparam name="T">The domain model type persisted in the MongoDB collection.</typeparam>
public abstract class MongoRepository<T> : IMongoRepository<T>
{
    /// <summary>
    /// The MongoDB database instance used by the repository.
    /// </summary>
    protected readonly IMongoDatabase Db;

    private IMongoCollection<T>? _collection;

    // protected MongoRepository(IOptions<DatabaseSettings> dbSettings)
    // {
    //     Db = MongoDbHostBuilderExtension.GetMongoDb(dbSettings);
    // }

    /// <summary>
    /// Initializes a new instance of the <see cref="MongoRepository{T}"/> class using the provided database.
    /// </summary>
    /// <param name="database">The MongoDB database instance used to access collections. Must not be <see langword="null"/>.</param>
    protected MongoRepository(IMongoDatabase database)
    {
        Db = database;
    }

    /// <summary>
    /// When implemented in a derived class, returns the name of the MongoDB collection used to store documents of type <typeparamref name="T"/>.
    /// </summary>
    /// <returns>The MongoDB collection name as a string.</returns>
    public abstract string GetCollectionName();

    /// <summary>
    /// Gets the underlying <see cref="IMongoCollection{TDocument}"/> for the repository.
    /// On first access, ensures the collection exists (creating it if necessary), then triggers asynchronous index creation
    /// and initial data loading via <see cref="CreateIndexes"/> and <see cref="InsertInitialLoad"/>.
    /// </summary>
    /// <returns>The MongoDB collection of <typeparamref name="T"/> documents.</returns>
    public IMongoCollection<T> Collection
    {
        get
        {
            if (_collection == null)
            {
                var collectionName = GetCollectionName();
                if (!CollectionExists(collectionName)) Db.CreateCollection(collectionName);
                _collection = Db.GetCollection<T>(collectionName);
                _ = CreateIndexes();
                _ = InsertInitialLoad();
            }

            return _collection;
        }
        set => _collection = value;
    }

    /// <summary>
    /// Asynchronously inserts a single document into the collection. Errors are logged and swallowed.
    /// </summary>
    /// <param name="obj">The document of type <typeparamref name="T"/> to insert.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous insert operation.</returns>
    public virtual async Task InsertOneAsync(T obj)
    {
        try
        {
            await Collection.InsertOneAsync(obj);
        }
        catch (Exception ex)
        {
            Log.Error("An error occurred: {ExMessage}", ex.Message);
        }
    }

    /// <summary>
    /// Asynchronously replaces (or upserts) a single document identified by its <c>_id</c>.
    /// Uses <see cref="ReplaceOptions"/> with <see cref="ReplaceOptions.IsUpsert"/> set to <see langword="true"/>
    /// so that the document is created if it does not exist. Errors are logged and swallowed.
    /// </summary>
    /// <param name="id">The <see cref="ObjectId"/> value of the document's <c>_id</c> field.</param>
    /// <param name="obj">The replacement document of type <typeparamref name="T"/>.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous replace/upsert operation.</returns>
    public async Task UpdateOneAsync(ObjectId id, T obj)
    {
        try
        {
            var filter = Builders<T>.Filter.Eq("_id", id);

            await Collection.ReplaceOneAsync(filter, obj, new ReplaceOptions { IsUpsert = true });
        }
        catch (Exception ex)
        {
            Log.Error("Error updating id: {Id}. Exception:{Ex}, stackTrace: {Trace}", id.ToString(), ex.Message,
                ex.StackTrace);
        }
    }

    /// <summary>
    /// Asynchronously finds and deletes a single document identified by its <c>_id</c>.
    /// Returns the deleted document, or the default value of <typeparamref name="T"/> if not found or on error.
    /// </summary>
    /// <param name="id">The <see cref="ObjectId"/> value of the document's <c>_id</c> field.</param>
    /// <returns>
    /// A <see cref="Task{T}"/> representing the asynchronous operation.
    /// The task result contains the deleted document, or <see langword="null"/> / default if no document matched or an error occurred.
    /// </returns>
    public async Task<T?> DeleteAsync(ObjectId id)
    {
        try
        {
            var filter = Builders<T>.Filter.Eq("_id", id);
            return await Collection.FindOneAndDeleteAsync(filter);
        }
        catch (Exception ex)
        {
            Log.Error("An error occurred: {ExMessage}", ex.Message);
            return default;
        }
    }

    /// <summary>
    /// Creates the indexes required for the collection. The base implementation is a no-op;
    /// derived classes should override this method to define their own indexes.
    /// </summary>
    /// <returns>A <see cref="Task"/> representing the asynchronous index creation operation.</returns>
    public virtual Task CreateIndexes()
    {
        return Task.CompletedTask;
    }

    /// <summary>
    /// Performs an initial data load (seeding) for the collection. The base implementation is a no-op;
    /// derived classes should override this method to provide custom seeding logic.
    /// </summary>
    /// <returns>A <see cref="Task"/> representing the asynchronous seeding operation.</returns>
    public virtual Task InsertInitialLoad()
    {
        return Task.CompletedTask;
    }

    /// <summary>
    /// Asynchronously inserts multiple documents into the collection using unordered semantics
    /// (a single failed insert does not abort the batch). Errors are logged and swallowed.
    /// </summary>
    /// <param name="obj">The list of documents of type <typeparamref name="T"/> to insert.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous bulk insert operation.</returns>
    public virtual async Task InsertManyAsync(List<T> obj)
    {
        try
        {
            InsertManyOptions options = new() { IsOrdered = false };
            await Collection.InsertManyAsync(obj, options);
        }
        catch (Exception ex)
        {
            Log.Error("An error occurred: {ExMessage}", ex.Message);
        }
    }

    /// <summary>
    /// Asynchronously updates all documents in the collection where the specified field equals <paramref name="oldId"/>,
    /// setting that field to the new <paramref name="id"/>. Errors are logged and swallowed.
    /// </summary>
    /// <param name="nameId">The name of the field to match and update.</param>
    /// <param name="id">The new <see cref="ObjectId"/> value to assign.</param>
    /// <param name="oldId">The existing <see cref="ObjectId"/> value to replace.</param>
    /// <returns>A <see cref="Task"/> representing the asynchronous update operation.</returns>
    protected async Task UpdateManyObjectIdAsync(string nameId, ObjectId id, ObjectId oldId)
    {
        try
        {
            var update = Builders<T>.Update.Set(nameId, id);
            var filter = Builders<T>.Filter.Eq(nameId, oldId);

            await Collection.UpdateManyAsync(filter, update);
        }
        catch (Exception ex)
        {
            Log.Error("An error occurred: {ExMessage}", ex.Message);
        }
    }

    /// <summary>
    /// Asynchronously finds and deletes a single document identified by a string <c>_id</c>.
    /// Returns the deleted document, or the default value of <typeparamref name="T"/> if not found or on error.
    /// </summary>
    /// <param name="id">The string value of the document's <c>_id</c> field.</param>
    /// <returns>
    /// A <see cref="Task{T}"/> representing the asynchronous operation.
    /// The task result contains the deleted document, or <see langword="null"/> / default if no document matched or an error occurred.
    /// </returns>
    protected async Task<T?> DeleteAsync(string id)
    {
        try
        {
            var filter = Builders<T>.Filter.Eq("_id", id);
            return await Collection.FindOneAndDeleteAsync(filter);
        }
        catch (Exception ex)
        {
            Log.Error("An error occurred: {ExMessage}", ex.Message);
            return default;
        }
    }

    /// <summary>
    /// Determines whether a collection with the specified name exists in the current database.
    /// Errors are logged and the method returns <see langword="false"/> in that case.
    /// </summary>
    /// <param name="collectionName">The name of the collection to check.</param>
    /// <returns>
    /// <see langword="true"/> if a collection with the given name exists; otherwise, <see langword="false"/>.
    /// Returns <see langword="false"/> when an exception is thrown while querying the database.
    /// </returns>
    protected bool CollectionExists(string collectionName)
    {
        try
        {
            var filter = new BsonDocument("name", collectionName);
            var options = new ListCollectionNamesOptions { Filter = filter };

            return Db.ListCollectionNames(options).Any();
        }
        catch (Exception ex)
        {
            Log.Error("An error occurred: {ExMessage}", ex.Message);
            return false;
        }
    }
}
