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

namespace adas_core.Infrastructure.Repositories;

/// <summary>
/// Repository for managing patient appointments in a MongoDB database. 
/// This class provides methods to perform CRUD operations and queries related to patient appointments, such as retrieving appointments by patient ID, finding appointments by point of care, and updating or deleting appointments. 
/// It utilizes the MongoDB driver for database interactions and is configured using application settings for collection names and other parameters.
/// </summary>
public class AppointmentRepository : MongoRepository<PatientAppointment>, IAppointmentRepository
{
    /// <summary>
    /// Holds the API settings for the repository, including collection names and other configuration parameters.
    /// </summary>
    private readonly ApiSettings _apiSettings;

    /// <summary>
    /// Initializes a new instance of the <see cref="AppointmentRepository"/> class with the specified API settings and MongoDB database.
    /// </summary>
    /// <param name="apiSettings">The API settings containing configuration for the repository.</param>
    /// <param name="database">The MongoDB database instance.</param>
    /// <exception cref="ArgumentNullException">Thrown when any of the input parameters are null.</exception>
    public AppointmentRepository(
        IOptions<ApiSettings> apiSettings,
        IMongoDatabase database) : base(database)
    {
        if (apiSettings == null) throw new ArgumentNullException(nameof(apiSettings));
        _apiSettings = apiSettings.Value;
    } //For testing


    /// <summary>
    /// Gets the name of the MongoDB collection for patient appointments. This method retrieves the collection name from the API settings, allowing for flexible configuration. 
    /// If the collection name is not specified in the settings, it defaults to "patients_appointments".
    /// </summary>
    /// <returns>The name of the MongoDB collection for patient appointments.</returns>
    public override string GetCollectionName()
    {
        return _apiSettings.PatientsAppointments ?? "patients_appointments";
    }

    /// <summary>
    /// Retrieves a list of patient appointments for a given patient ID. This method constructs a filter to query the MongoDB collection based on the patient ID and sorts the results by creation time in descending order.
    /// </summary>
    /// <param name="patientId">The ID of the patient whose appointments are to be retrieved.</param>
    /// <returns>A list of patient appointments for the specified patient ID.</returns>
    public async Task<List<PatientAppointment>> GetByPatient(ObjectId patientId)
    {
        var filter = Builders<PatientAppointment>.Filter.Eq(ob => ob.PatientId, patientId);
        var options = new FindOptions<PatientAppointment>
        {
            Sort = Builders<PatientAppointment>.Sort.Descending("createTime")
        };

        var result = await Collection.FindAsync(filter, options);

        return result.ToList();
    }

    /// <summary>
    /// Finds patient appointments based on a given point of care (PoC). This method constructs a filter to query the MongoDB collection for appointments that match the specified point of care, which includes details such as bed, room, and unit name. It utilizes the FindByLocation method to perform the actual query based on the constructed patient location.
    /// </summary>
    /// <param name="poc">The point of care details to filter appointments.</param>
    /// <returns>A list of patient appointments that match the specified point of care.</returns>
    public async Task<List<PatientAppointment>> FindByPoC(PointOfCare poc)
    {
        var location = new PatientLocation()
        {
            Bed = poc.Bed,
            Room = poc.Room,
            UnitName = poc.UnitName
        };
        return await FindByLocation(location);
    }

    /// <summary>
    /// Inserts a new patient appointment into the MongoDB collection. This method ensures that the creation time of the appointment is set to the current UTC time if it is not already specified before inserting the appointment into the database using the base class's InsertOneAsync method.
    /// </summary>
    /// <param name="appointment">The patient appointment to be inserted.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public override async Task InsertOneAsync(PatientAppointment appointment)
    {
        appointment.CreateTime ??= DateTime.UtcNow;
        await base.InsertOneAsync(appointment);
    }

    /// <summary>
    /// Updates an existing patient appointment in the MongoDB collection. This method takes a patient appointment object as input and updates the corresponding document in the database based on the appointment's ID. 
    /// It uses the UpdateOneAsync method from the base class to perform the update operation.
    /// </summary>
    /// <param name="appointment">The patient appointment to be updated.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public async Task Update(PatientAppointment appointment)
    {
        await UpdateOneAsync(appointment.Id, appointment);
    }

    /// <summary>
    /// Deletes a patient appointment from the MongoDB collection based on the appointment's ID. This method constructs a filter to identify the document to be deleted using the appointment's ID and then calls the DeleteOneAsync method to remove the document from the database.
    /// </summary>
    /// <param name="id">The ID of the patient appointment to be deleted.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public new async Task DeleteAsync(ObjectId id)
    {
        var filter = Builders<PatientAppointment>.Filter.Eq(t => t.Id, id); // Replace 'T' with your actual class name.
        await Collection.DeleteOneAsync(filter);
    }

    /// <summary>
    /// Finds patient appointments based on the patient ID. This method constructs a filter to query the MongoDB collection for appointments that match the specified patient ID and returns an asynchronous cursor to iterate through the results.
    /// </summary>
    /// <param name="patientId">The ID of the patient whose appointments are to be retrieved.</param>
    /// <returns>An asynchronous cursor to iterate through the patient appointments.</returns>
    public Task<IAsyncCursor<PatientAppointment>> FindByPatientIdAsync(ObjectId patientId)
    {
        var filter = Builders<PatientAppointment>.Filter.Eq(ob => ob.PatientId, patientId);
        return Collection.FindAsync(filter);
    }

    /// <summary>
    /// Deletes all patient appointments associated with a specific patient ID. This method constructs a filter to identify all documents in the MongoDB collection that match the specified patient ID and then calls the DeleteManyAsync method to remove all matching documents from the database.
    /// </summary>
    /// <param name="patientId">The ID of the patient whose appointments are to be deleted.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public async Task DeleteByPatientId(ObjectId patientId)
    {
        var filter = Builders<PatientAppointment>.Filter.Eq(po => po.PatientId, patientId);
        await Collection.DeleteManyAsync(filter);
    }

    /// <summary>
    /// Finds a patient appointment based on the patient ID and visit number. This method constructs a filter to query the MongoDB collection for an appointment that matches both the specified patient ID and visit number, and returns the first matching appointment if found, or null if no match is found.
    /// </summary>
    /// <param name="patientId">The ID of the patient whose appointment is to be retrieved.</param>
    /// <param name="visitNumber">The visit number of the appointment to be retrieved.</param>
    /// <returns>The first matching patient appointment if found, or null if no match is found.</returns>
    public async Task<PatientAppointment?> FindByPatientAndVisitNumber(ObjectId patientId, string visitNumber)
    {
        var builder = Builders<PatientAppointment>.Filter;
        var filter = builder.And(
            builder.Eq(ob => ob.PatientId, patientId),
            builder.Eq(ob => ob.VisitNumber, visitNumber)
        );

        var result = await Collection.FindAsync(filter);

        return await result.FirstOrDefaultAsync();
    }

    /// <summary>
    /// Finds a patient appointment based on the patient ID and appointment reason. This method constructs a filter to query the MongoDB collection for an appointment that matches both the specified patient ID and appointment reason, and returns the first matching appointment if found, or null if no match is found.
    /// </summary>
    /// <param name="patientId">The ID of the patient whose appointment is to be retrieved.</param>
    /// <param name="appointmentReason">The reason for the appointment to be retrieved.</param>
    /// <returns>The first matching patient appointment if found, or null if no match is found.</returns>
    public async Task<PatientAppointment?> FindByPatientAndReason(ObjectId patientId, string? appointmentReason)
    {
        var builder = Builders<PatientAppointment>.Filter;
        var filter = builder.And(
            builder.Eq(ob => ob.PatientId, patientId),
            builder.Eq(ob => ob.AppointmentReason, appointmentReason)
        );

        var result = await Collection.FindAsync(filter);

        return await result.FirstOrDefaultAsync();
    }

    /// <summary>
    /// Finds patient appointments based on a given patient location. This method constructs a filter to query the MongoDB collection for appointments that match the specified patient location, which includes details such as bed, room, and unit name. It uses the Builders class to create a filter that checks for the existence of the Locations field and matches the specified location details within the ResourceGroups of the appointments.
    /// </summary>
    /// <param name="location">The location details to filter patient appointments by.</param>
    /// <returns>A list of patient appointments that match the specified location.</returns>
    public async Task<List<PatientAppointment>> FindByLocation(PatientLocation location)
    {
        var builder = Builders<PatientAppointmentResourceGroup>.Filter;
        var existsFilter = builder.Exists(rg => rg.Locations);
        var locationFilter = builder.ElemMatch(rg => rg.Locations,
            l => l.Bed == location.Bed && l.UnitName == location.UnitName);

        var combinedFilter = builder.And(existsFilter, locationFilter);

        var filter = Builders<PatientAppointment>.Filter.ElemMatch(pa => pa.ResourceGroups, combinedFilter);

        var result = await Collection.FindAsync(filter);
        return result?.ToList()??[];
    }

    /// <summary>
    /// Updates the ObjectId references in patient appointments when a patient's ID changes. This method constructs a filter to identify all documents in the MongoDB collection that match the specified old patient ID and then updates those documents to reference the new patient ID using the UpdateManyAsync method.
    /// </summary>
    /// <param name="nameId">The name ID associated with the patient appointments to be updated.</param>
    /// <param name="id">The new ObjectId to replace the old patient ID.</param>
    /// <param name="oldId">The old ObjectId of the patient whose appointments are to be updated.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId)
    {
        await UpdateManyObjectIdAsync(nameId, id, oldId);
    }

    /// <summary>
    /// Creates indexes for the patient appointments collection in MongoDB. This method defines a list of indexes to be created, including an index on the patient ID field, and then calls the EnsureIndexes method from the MongoUtils class to ensure that the specified indexes are created in the database. 
    /// The indexes are created in the background and are not unique.
    /// </summary>
    /// <returns>A task representing the asynchronous operation.</returns>
    public override async Task CreateIndexes()
    {
        var options = new CreateIndexOptions { Background = true, Unique = false };
        var indexes = new List<CreateIndexModel<PatientAppointment>>
        {
            new("{ patientid: 1 }", options)
        };

        await MongoUtils.EnsureIndexes(Collection, indexes);
    }
}