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

namespace adas_core.Infrastructure.Repositories;

/// <summary>
/// Repository for managing Discharge entities in MongoDB. Provides methods for CRUD operations and specific queries related to discharges.
/// </summary>
public class DischargeRepository : MongoRepository<Discharge>, IDischargeRepository
{
    private readonly ApiSettings _apiSettings;

    /// <summary>
    /// Initializes a new instance of the DischargeRepository 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>
    public DischargeRepository(IOptions<ApiSettings> apiSettings, IMongoDatabase database) : base(database)
    {
        _apiSettings = apiSettings.Value;
    }

    /// <summary>
    /// Gets the name of the MongoDB collection for discharges from the API settings.
    /// </summary>
    /// <returns>The name of the MongoDB collection for discharges.</returns>
    public override string GetCollectionName()
    {
        return _apiSettings.Discharges;
    }

    /// <summary>
    /// Inserts a new discharge record into the MongoDB collection. Sets the discharge date to the current UTC time before insertion.
    /// </summary>
    /// <param name="discharge">The discharge record to insert.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public override async Task InsertOneAsync(Discharge discharge)
    {
        try
        {
            discharge.DischargeDate = DateTime.UtcNow;
            await base.InsertOneAsync(discharge);
        }
        catch (Exception e)
        {
            Log.Warning("Exception trying to insert  discharge: {discharge}. Exception {e}", discharge, e);
        }
    }

    /// <summary>
    /// Deletes a discharge record from the MongoDB collection based on the specified ID.
    /// </summary>
    /// <param name="id">The ID of the discharge record to delete.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public async Task Delete(ObjectId id)
    {
        try
        {
            var filter = Builders<Discharge>.Filter.Eq(x => x.Id, id);
            await Collection.DeleteOneAsync(filter, null);
        }
        catch (Exception e)
        {
            Log.Error("Exception trying to delete discharge: {id}. Exception {e}", id, e);
            throw;
        }
    }

    /// <summary>
    /// Updates an existing discharge record in the MongoDB collection. If the update operation fails, a ConflictException is thrown.
    /// </summary>
    /// <param name="discharge">The discharge record to update.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    /// <exception cref="ConflictException"></exception>
    public async Task Update(Discharge discharge)
    {
        try
        {
            await UpdateOneAsync(discharge.Id, discharge);
        }
        catch (Exception e)
        {
            Log.Error("Exception trying to update discharge: {discharge}. Exception {e}", discharge, e);
            throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
        }
    }

    /// <summary>
    /// Updates the unit name of a discharge record in the MongoDB collection based on the specified ID. If the update operation fails, an error is logged.
    /// </summary>
    /// <param name="id">The ID of the discharge record to update.</param>
    /// <param name="unit">The new unit name to set.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public async Task UpdateUnit(ObjectId id, string unit)
    {
        var filterBuilder = Builders<Discharge>.Filter;
        var filter = filterBuilder.Eq(p => p.Id, id);

        var update = Builders<Discharge>.Update
            .Set(p => p.PatientLocation!.UnitName, unit);

        await Collection.UpdateOneAsync(filter, update);
    }

    /// <summary>
    /// Updates the patient information of a discharge record in the MongoDB collection based on the specified ID. If the update operation fails, an error is logged.
    /// </summary>
    /// <param name="id">The ID of the discharge record to update.</param>
    /// <param name="patient">The new patient information to set.</param>
    /// <returns>A task representing the asynchronous operation.</returns>
    public async Task UpdatePatient(ObjectId id, Patient patient)
    {
        var filterBuilder = Builders<Discharge>.Filter;
        var filter = filterBuilder.Eq(p => p.Id, id);

        var update = Builders<Discharge>.Update
            .Set(p => p.Patient, patient);

        await Collection.UpdateOneAsync(filter, update);
    }

    /// <summary>
    /// Finds and retrieves all discharge records from the MongoDB collection. If an error occurs during retrieval, an error is logged and an empty list is returned.
    /// </summary>
    /// <returns>A task representing the asynchronous operation, containing a list of all discharge records.</returns>
    public async Task<IEnumerable<Discharge>> FindAll()
    {
        try
        {
            var result = await Collection.FindAsync(_ => true);
            return await result.ToListAsync();
        }
        catch (Exception ex)
        {
            Log.Error("Error getting all discharges. Exception: {ex}", ex);
            return [];
        }
    }

    /// <summary>
    /// Finds and retrieves a discharge record from the MongoDB collection based on the specified ID. If an error occurs during retrieval, an error is logged and null is returned.
    /// </summary>
    /// <param name="id">The ID of the discharge record to retrieve.</param>
    /// <returns>A task representing the asynchronous operation, containing the discharge record if found, or null if not found or an error occurs.</returns>
    public async Task<Discharge?> FindById(ObjectId id)
    {
        try
        {
            var filter = Builders<Discharge>.Filter.Eq(p => p.Id, id);
            var result = await Collection.FindAsync(filter);

            return await result.FirstOrDefaultAsync();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching discharge by id: {id}. Exception: {ex}", id, ex);
            return null;
        }
    }

    /// <summary>
    /// Finds and retrieves discharge records from the MongoDB collection based on the specified unit name. If an error occurs during retrieval, an error is logged and null is returned.
    /// </summary>
    /// <param name="unit">The unit name to search for.</param>
    /// <returns>A task representing the asynchronous operation, containing a list of discharge records matching the specified unit name, or null if an error occurs.</returns>
    public async Task<IEnumerable<Discharge>?> FindByUnit(string unit)
    {
        try
        {
            var filter = Builders<Discharge>.Filter.Eq(p => p.PatientLocation!.UnitName, unit);
            var result = await Collection.FindAsync(filter);

            return result.ToEnumerable();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching discharge by Unit id: {unit}. Exception: {ex}", unit, ex);
            return null;
        }
    }

    /// <summary>
    /// Counts the number of discharge records in the MongoDB collection that match the specified unit ID. If an error occurs during counting, an error is logged and 0 is returned.
    /// </summary>
    /// <param name="unitId">The ID of the unit to count discharge records for.</param>
    /// <returns>A task representing the asynchronous operation, containing the count of discharge records matching the specified unit ID, or 0 if an error occurs.</returns>
    public async Task<long> CountByUnitId(ObjectId unitId)
    {
        try
        {
            return await Collection.CountDocumentsAsync(
                Builders<Discharge>.Filter.Eq(p => p.UnitId, unitId));
        }
        catch (Exception e)
        {
            Log.Error(e.Message);
            return 0;
        }
    }

    /// <summary>
    /// Finds and retrieves discharge records from the MongoDB collection based on the specified destination. If an error occurs during retrieval, an error is logged and null is returned.
    /// </summary>
    /// <param name="destination">The destination to search for.</param>
    /// <returns>A task representing the asynchronous operation, containing a list of discharge records matching the specified destination, or null if an error occurs.</returns>
    public async Task<IEnumerable<Discharge>?> FindByDestination(string destination)
    {
        try
        {
            var filter = string.IsNullOrEmpty(destination)
                ? Builders<Discharge>.Filter.Empty
                : Builders<Discharge>.Filter.Eq(p => p.Destination, destination);

            var result = await Collection.FindAsync(filter);

            return result.ToEnumerable();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching discharges by destination: {destination}. Exception: {ex}", destination, ex);
            return null;
        }
    }

    /// <summary>
    /// Finds and retrieves discharge records from the MongoDB collection based on the specified unit IDs. If an error occurs during retrieval, an error is logged and null is returned.
    /// </summary>
    /// <param name="unitIds">The IDs of the units to search for.</param>
    /// <returns>A task representing the asynchronous operation, containing a list of discharge records matching the specified unit IDs, or null if an error occurs.</returns>
    public async Task<IEnumerable<Discharge>?> GetDischargesByUnitIds(IEnumerable<ObjectId>? unitIds)
    {
        var filterUnit = Builders<Discharge>.Filter.In("unitId", unitIds);
        return await Collection.Find(filterUnit).ToListAsync();
    }

    /// <summary>
    /// Finds and retrieves discharge records from the MongoDB collection based on the specified Point of Care ID. If an error occurs during retrieval, an error is logged and null is returned.
    /// </summary>
    /// <param name="pocId">The ID of the Point of Care to search for.</param>
    /// <returns>A task representing the asynchronous operation, containing a list of discharge records matching the specified Point of Care ID, or null if an error occurs.</returns>
    public async Task<IEnumerable<Discharge>?> FindByPoCId(ObjectId pocId)
    {
        try
        {
            var filter = Builders<Discharge>.Filter.Eq(p => p.PointOfCareId, pocId);

            var result = await Collection.FindAsync(filter);

            return result.ToEnumerable();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching discharges by PointOfCare: {destination}. Exception: {ex}", pocId, ex);
            return null;
        }
    }
    
    /// <summary>
    /// Finds and retrieves discharge records from the MongoDB collection based on the specified service. If an error occurs during retrieval, an error is logged and null is returned.
    /// </summary>
    /// <param name="service">The service to search for.</param>
    /// <returns>A task representing the asynchronous operation, containing a list of discharge records matching the specified service, or null if an error occurs.</returns>   
    public async Task<IEnumerable<Discharge>?> FindByService(string service)
    {
        try
        {
            var filter = Builders<Discharge>.Filter.Eq(p => p.Service, service);
            var result = await Collection.FindAsync(filter);

            return result.ToEnumerable();
        }
        catch (Exception ex)
        {
            Log.Error("Error searching discharges by service: {origin}. Exception: {ex}", service, ex);
            return null;
        }
    }

    /// <summary>
    /// Finds and retrieves a discharge record from the MongoDB collection based on the specified patient location. If an error occurs during retrieval, an error is logged and null is returned.
    /// </summary>
    /// <param name="location">The patient location to search for.</param>
    /// <returns>A task representing the asynchronous operation, containing the discharge record matching the specified patient location, or null if an error occurs.</returns>
    public async Task<Discharge?> GetDischargeByLocation(PatientLocation location)
    {
        try
        {
            var filter = Builders<Discharge>.Filter.Eq(p => p.PatientLocation, location);
            var result = await Collection.FindAsync(filter);

            return await result.FirstOrDefaultAsync();
        }
        catch (Exception e)
        {
            Log.Error($"Unable to get discharge by location on repository Exception: {e}");
            return null;
        }
    }

    /// <summary>
    /// Finds and retrieves a discharge record from the MongoDB collection based on the specified Point of Care ID. If an error occurs during retrieval, an error is logged and null is returned.
    /// </summary>
    /// <param name="id">The ID of the Point of Care to search for.</param>
    /// <returns>A task representing the asynchronous operation, containing the discharge record matching the specified Point of Care ID, or null if an error occurs.</returns>
    public async Task<Discharge?> GetDischargeByPointOfCareId(ObjectId id)
    {
        try
        {
            var filter = Builders<Discharge>.Filter.Eq(p => p.PointOfCareId, id);
            var result = await Collection.FindAsync(filter);

            return await result.FirstOrDefaultAsync();
        }
        catch (Exception e)
        {
            Log.Error($"Unable to get discharge by location on repository Exception: {e}");
            return null;
        }
    }

    /// <summary>
    /// Updates the master list options for discharge records in the MongoDB collection based on the specified unit IDs, update option, and master list type. 
    /// The method parses the master list type and performs updates accordingly. If an error occurs during the update process, an error is logged and an empty list is returned.
    /// </summary>
    /// <param name="unitIds">The list of unit IDs for which to update the master list options.</param>
    /// <param name="opt">The update option specifying the changes to be applied to the master list.</param>
    /// <param name="typeName">The name of the master list type to be updated.</param>
    /// <returns>A task representing the asynchronous operation, containing the updated discharge records, or an empty list if an error occurs.</returns>
    public Task<IEnumerable<Discharge>> UpdateMasterListOption(List<ObjectId> unitIds, UpdateOptionMasterListDto opt,
        string typeName)
    {
        var isParsed = Enum.TryParse<MasterListType>(typeName, out var parsedTypeName);
        if (isParsed)
            switch (parsedTypeName)
            {
                case MasterListType.DestinationList:
                    // destination
                    // destinationOption
                    break;
                case MasterListType.ServiceList:
                    // service
                    break;
            }

        return Task.FromResult<IEnumerable<Discharge>>(new List<Discharge>());
    }

    /// <summary>
    /// Deletes discharge records from the MongoDB collection based on the specified unit ID. If an error occurs during deletion, an error is logged and false is returned; otherwise, true is returned upon successful deletion.
    /// </summary>
    /// <param name="unitId">The ID of the unit for which to delete discharge records.</param>
    /// <returns>A task representing the asynchronous operation, containing true if the deletion was successful, or false if an error occurred.</returns>
    public async Task<bool> DeleteByUnitId(ObjectId unitId)
    {
        try
        {
            var filter = Builders<Discharge>.Filter.Where(p => p.UnitId == unitId);
            await Collection.DeleteManyAsync(filter);
            return true;
        }
        catch (Exception ex)
        {
            Log.Error(ex.Message);
            return false;
        }
    }

    /// <summary>
    /// Deletes master list options for discharge records in the MongoDB collection based on the specified unit IDs, update option, and master list type.
    /// </summary>
    /// <param name="unitIds">The list of unit IDs for which to delete master list options.</param>
    /// <param name="opt">The update option specifying the changes to be applied to the master list.</param>
    /// <param name="typeName">The name of the master list type to be updated.</param>
    /// <returns>A task representing the asynchronous operation, containing the updated discharge records, or an empty list if an error occurs.</returns>
    public Task<IEnumerable<Discharge>> DeleteMasterListOption(List<ObjectId> unitIds, OptionList opt, string typeName)
    {
        var isParsed = Enum.TryParse<MasterListType>(typeName, out var parsedTypeName);
        if (isParsed)
            switch (parsedTypeName)
            {
                case MasterListType.DestinationList:
                    // destination
                    // destinationOption
                    break;
                case MasterListType.ServiceList:
                    // service
                    break;
            }

        return Task.FromResult<IEnumerable<Discharge>>(new List<Discharge>());
    }

    /// <summary>
    /// Finds and retrieves a discharge record from the MongoDB collection based on the specified patient ID. If an error occurs during retrieval, an error is logged and null is returned.
    /// </summary>
    /// <param name="patientId">The ID of the patient for which to retrieve the discharge record.</param>
    /// <returns>A task representing the asynchronous operation, containing the discharge record if found, or null if an error occurs or the record is not found.</returns>
    public async Task<Discharge?> GetByPatientId(ObjectId patientId)
    {
        try
        {
            var filter = Builders<Discharge>.Filter.Eq(p => p.PatientId, patientId);
            var result = await Collection.FindAsync(filter);

            return await result.FirstOrDefaultAsync();
        }
        catch (Exception e)
        {
            Log.Error($"Unable to get discharge by patient id on repository Exception: {e}");
            return null;
        }
    }

    /// <summary>
    /// Creates indexes for the MongoDB collection based on the specified fields and options.
    /// The method ensures that unique indexes are created for the medical discharge and admin discharge fields, with a partial filter expression to only include documents where both fields exist. 
    /// If an error occurs during index creation, an error is logged.
    /// </summary>
    /// <returns></returns>
    public override async Task CreateIndexes()
    {
        var optionsUq = new CreateIndexOptions<Discharge>
        {
            Background = true,
            Unique = true,
            PartialFilterExpression = Builders<Discharge>.Filter.Exists(p => p.MedicalDischarge) &
                                      Builders<Discharge>.Filter.Exists(p => p.AdminDischarge)
        };
        var indexes = new List<CreateIndexModel<Discharge>>
        {
            new("{ medicalDischarge: 1 }", optionsUq),
            new("{ adminDischarge: 1 }", optionsUq)
        };

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