using adas_core.Application.Exceptions;
using adas_core.Application.Services.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.MongoModels;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using Serilog;
using Patient = adas_core.Domain.Models.MongoModels.Patient;

namespace adas_core.Application.Services;

public class AdminPanelService(
    IOptions<ApiSettings> apiSettings,
    IPatientService patientService,
    IConfigObservationService configObservationService,
    IMedicineService medicineService,
    IPointOfCareService pocService,
    IUnitService unitService,
    ILogger<AdminPanelService> logger,
    IAdmissionService admissionService,
    IAuthService authService,
    IDischargeService dischargeService,
    IDisplayService displayService)
    : IAdminPanelService
{
    private readonly List<string> _defaultIdRecord = apiSettings.Value.DefaultIdRecord ?? [];


    #region Patient

    /// <summary>
    /// Archives the specified patient via the patient service and logs the operation. Returns <c>true</c> on success; any exception thrown by the underlying service is written to the console and rethrown.
    /// </summary>
    /// <param name="patient">The patient to archive.</param>
    /// <returns><c>true</c> if the patient was archived successfully.</returns>
    /// <exception cref="System.Exception">Rethrows any exception thrown by the underlying patient service after logging it to the console.</exception>
    public async Task<bool> ArchivePatient(Patient patient)
        {
            try
            {
                await patientService.ArchivePatient(patient);
    
                logger.LogDebug("archived patientid {patientid} from ADMPanel", patient.Id);
                return true;
            }
            catch (Exception e)
            {
                Console.WriteLine(e);
                throw;
            }
        }

    /// <summary>
    /// Creates a new patient from an ADMPanel request, generating a new identifier and persisting it through the patient service.
    /// </summary>
    /// <param name="admRequest">The ADMPanel request containing the data used to populate the new patient.</param>
    /// <returns>The newly created <see cref="Patient"/> with its generated identifier.</returns>
    public async Task<Patient?> CreatePatient(AdmPanelRequest admRequest)
        {
            var patient = new Patient
            {
                Id = ObjectId.GenerateNewId()
            };
            await UpdateNewPatient(patient, admRequest);
            await patientService.Insert(patient);
    
            logger.LogDebug("Inserted {patientid} from ADMPanel", patient.Id);
            return patient;
        }

    /// <summary>
    /// Updates an existing <see cref="Patient"/> with data from an <see cref="AdmPanelRequest"/>, applying patient fields,
    /// person data, default identifier records, and unit/point-of-care (location) assignment, including conflict and not-found validations.
    /// </summary>
    /// <param name="patient">The patient entity to be updated in place.</param>
    /// <param name="admRequest">The admission panel request containing the new values to apply to the patient.</param>
    /// <exception cref="Exception">Thrown when the target <c>PointOfCareId</c> is already occupied by another patient.</exception>
    /// <exception cref="NotFoundException">Thrown when the requested <c>PointOfCareId</c> does not exist.</exception>
    /// <returns>The asynchronous <see cref="Task"/> representing the update operation.</returns>
    private async Task UpdateNewPatient(Patient patient, AdmPanelRequest admRequest)
        {
            if (!string.IsNullOrEmpty(admRequest.PatientNumber)) patient.PatientNumber = admRequest.PatientNumber;
            if (admRequest.AdmTime.HasValue) patient.AdmTime = admRequest.AdmTime;
            if (admRequest.Patient != null && !admRequest.Patient.IsEmptyDontCheckIds())
                patient.Person = admRequest.Patient;
            if (admRequest.Patient != null && (admRequest.Patient.Ids == null || admRequest.Patient.Ids.Count == 0))
            {
                if (patient.Person is { Ids: null }) patient.Person.Ids = new Dictionary<string, string>();
                foreach (var item in _defaultIdRecord) patient.Person?.Ids?.Add(item, admRequest?.PatientNumber ?? "null");
            }
    
            patient.Person?.SetIds(patient.Person?.Ids ?? new Dictionary<string, string>());
    
            if (admRequest is { UnitId: not null })
            {
                var unit = await unitService.FindById(admRequest.UnitId);
                patient.UnitId = unit?.Id ?? admRequest.UnitId;
                patient.UnitString = unit?.Name;
    
    
                if (admRequest is { PointOfCareId: not null })
                {
                    //El paciente existe en la localizacion no te dejo insertarlo
                    var patientInLocation = await patientService.FindByPointOfCareId(admRequest.PointOfCareId.Value);
                    if (patientInLocation != null)
                    {
                        logger.LogError(
                            "trying to insert patientid {patientid} in to location already in use. PointOfcareId: {poc}",
                            patient.Id, patient.PointOfCareId);
                        throw new Exception($"patient already in location: {patient.Location}");
                    }
    
                    var poc = await pocService.FindById(admRequest.PointOfCareId.Value);
                    if (poc == null)
                    {
                        logger.LogError(
                            "trying to insert patientid {patientid} in to location not found. PointOfcareId: {poc}",
                            patient.Id, admRequest.PointOfCareId);
                        throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
                    }
    
                    await pocService.SetPointOfCareStatus(poc.Id, StatusEnum.PointOfCare.InUse);
    
                    patient.Location = new PatientLocation
                    (
                        bed: poc.Bed,
                        room: poc.Room,
                        unitName: unit?.Name
                    );
                    patient.PointOfCareId = poc.Id;
                }
                else
                {
                    var pocUnknown =
                        await pocService.FindByBedAndUnitId(VirtualPointOfCare.Unknown.ToString(), patient.UnitId);
    
                    patient.PointOfCareId = pocUnknown?.Id;
                }
    
                patient.UpdateDate = DateTime.UtcNow;
            }
        }

    /// <summary>
    /// Retrieves a patient by their unique identifier from the patient service.
    /// Throws a not-found exception when no matching patient exists for the specified identifier.
    /// </summary>
    /// <param name="id">The unique identifier of the patient to look up.</param>
    /// <returns>The patient matching the provided identifier.</returns>
    /// <exception cref="NotFoundException">Thrown when no patient is found for the specified identifier.</exception>
    public async Task<Patient?> FindPatientById(ObjectId id)
        {
            return await patientService.FindById(id) ??
                   throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
        }

    /// <summary>
    /// Finds a patient at the specified location by delegating to the patient service.
    /// Returns null when no patient is found at the given location.
    /// </summary>
    /// <param name="location">The location to search for a patient at.</param>
    /// <returns>A <see cref="Patient"/> if one is found at the specified location; otherwise, <c>null</c>.</returns>
    public async Task<Patient?> FindPatientByLocation(PatientLocation location)
        {
            return await patientService.FindByLocation(location);
        }

    /// <summary>
    /// Retrieves a patient by their unique patient number by delegating to the patient service.
    /// Returns <c>null</c> when no matching patient is found.
    /// </summary>
    /// <param name="patientNumber">The unique patient number used to look up the patient.</param>
    /// <returns>A <see cref="Patient"/> if a match is found; otherwise, <c>null</c>.</returns>
    public async Task<Patient?> FindPatientByPatientNumber(string patientNumber)
        {
            return await patientService.FindByPatientNumber(patientNumber);
        }

    /// <summary>
    /// Updates the patient information based on the provided administrative panel request, persisting only the patient-related fields and detecting whether the patient number has changed.
    /// </summary>
    /// <param name="request">The administrative panel request containing the new patient number and the updated patient data to apply.</param>
    /// <param name="oldPatient">The existing patient record currently stored in the database that will be updated.</param>
    /// <returns>A task that resolves to <c>true</c> when the patient data is successfully updated.</returns>
    /// <exception cref="ConflictException">Thrown when the request is missing the patient number, the patient payload is null, or the patient payload is empty.</exception>
    public async Task<bool> UpdatePatientData(AdmPanelRequest request, Patient oldPatient)
        {
            //traer el paciente que se quiere actualizar con los valores que tenga en la base de datos a una variable
            //actualizar unicamente los campos que tengan que ver con los datos del paciente
            if (request.PatientNumber == null || request.Patient == null || request.Patient.IsEmptyDontCheckIds())
            {
                logger.LogError("Patient Data not updated. Old Patient:{oldPatient}. Api Request {request}", oldPatient,
                    request);
                throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
            }
    
            var patientNumberChanged = oldPatient.PatientNumber != request.PatientNumber;
            await patientService.UpdatePatientData(oldPatient.Id, request.PatientNumber, request.Patient,
                patientNumberChanged);
    
            return true;
        }

    public async Task<bool> UpdatePatientLocation(AdmPanelRequest request)
    {
        var patient = await patientService.FindByLocation(request.OldLocation);

        var patientExistsInLocation = await patientService.FindByLocation(request.Location);

        //Si ya hay un paciente en esa localización 
        if (patientExistsInLocation != null)
            //Si hay un paciente distinto en la nueva localización movemos al anterior
            if (patient != null && patientExistsInLocation.PatientNumber != patient.PatientNumber)
                await patientService.UpdateLocation(patientExistsInLocation.Id,
                    new PatientLocation(VirtualPointOfCare.Pushed.ToString(), ObjectId.GenerateNewId().ToString()));

        if (patient != null)
        {
            if (request.Location?.Bed == null || string.IsNullOrEmpty(request.Location.Bed))
                request.Location = new PatientLocation(VirtualPointOfCare.Pushed.ToString(),
                    ObjectId.GenerateNewId().ToString());


            //to solve problems when empty beds with not scapped ""
            request.Location.Bed = request.Location?.Bed?.Replace("\"", "");

            await patientService.UpdateLocation(patient.Id, request.Location);
            patient = await patientService.FindById(patient.Id);
            if (patient != null) await patientService.Update(patient);
        }
        else
        {
            patient = new Patient
            {
                Id = ObjectId.GenerateNewId()
            };
            await patientService.Insert(patient);
        }

        return true;
    }

    /// <summary>
    /// Asynchronously retrieves a <see cref="Patient"/> based on the provided admission panel request, using location-aware lookup when the location is not fully empty.
    /// When a location is present, the search is performed including location criteria; otherwise the location parameter is ignored.
    /// </summary>
    /// <param name="request">The admission panel request containing the patient identification data and optional location used to locate the patient.</param>
    /// <returns>The matching <see cref="Patient"/> if found.</returns>
    /// <exception cref="NotFoundException">Thrown when no patient matches the provided request criteria.</exception>
    public async Task<Patient?> FindPatient(AdmPanelRequest request)
        {
            var findByLocation = !request.Location?.IsFullEmpty();
    
            return await patientService.FindPatient(request.PatientId, request.PatientNumber, request.Location,
                findByLocation ?? false) ?? throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
        }

    /// <summary>
    /// Retrieves a patient associated with the specified location, throwing a not-found exception if no patient is found.
    /// </summary>
    /// <param name="location">The location used to look up the patient.</param>
    /// <returns>The patient found at the specified location.</returns>
    /// <exception cref="NotFoundException">Thrown when no patient is found for the given location.</exception>
    public async Task<Patient?> FindByLocation(PatientLocation location)
        {
            return await patientService.FindByLocation(location) ??
                   throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
        }

    #endregion

    #region ConfigObservations

    /// <summary>
    /// Creates a new configuration based on the provided observation data.
    /// Throws a conflict exception when the underlying creation operation fails (returns null), otherwise returns true.
    /// </summary>
    /// <param name="configObservation">The configuration observation containing the data to persist.</param>
    /// <returns>A task that resolves to <c>true</c> when the configuration is created successfully.</returns>
    /// <exception cref="ConflictException">Thrown when the configuration creation fails, indicated by a null result from the service call.</exception>
    public async Task<bool> CreateConfig(ConfigObservation configObservation)
        {
            _ = await configObservationService.CreateConfig(configObservation) ??
                throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed);
            return true;
        }

    /// <summary>
    /// Updates a configuration observation by delegating to the configuration service. If the service returns a null result, indicating a failure to update, a conflict exception is thrown.
    /// </summary>
    /// <param name="configObservation">The configuration observation to update.</param>
    /// <returns>A task that resolves to <c>true</c> when the configuration is successfully updated.</returns>
    /// <exception cref="ConflictException">Thrown when the underlying update operation fails, as indicated by a null result from the service.</exception>
    public async Task<bool> UpdateConfig(ConfigObservation configObservation)
        {
            _ = await configObservationService.UpdateConfig(configObservation) ??
                throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
            return true;
        }

    /// <summary>
    /// Deletes a config observation item by its identifier. Throws a conflict exception if the underlying removal service returns a null result, indicating the delete could not be completed.
    /// </summary>
    /// <param name="id">The unique identifier of the config observation item to delete.</param>
    /// <returns><c>true</c> when the config observation item is successfully removed.</returns>
    /// <exception cref="ConflictException">Thrown when the removal operation returns a null result, signaling a conflict with the delete request.</exception>
    public async Task<bool> DeleteConfigObservationItem(ObjectId id)
        {
            _ = await configObservationService.RemoveConfigItem(id) ??
                throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
            return true;
        }

    #endregion

    #region Unit

    /// <summary>
    /// Inserts a new unit and automatically creates the default Points of Care associated with it, one for each value of the <see cref="VirtualPointOfCare"/> enum.
    /// Each created Point of Care is set to Available status, using the enum value name for both Room and Bed, and its identifier is added to the unit's PointOfCareIds before updating the unit. Returns null if the initial unit insertion fails.
    /// </summary>
    /// <param name="unit">The unit to be inserted.</param>
    /// <returns>The inserted unit with its associated Point of Care identifiers, or null if the unit could not be inserted.</returns>
    public async Task<Unit?> InsertUnit(Unit unit)
        {
            //Insertamos la unidad y creamos los PoCs por defecto para esa unidad
            var result = await unitService.InsertOne(unit);
            if (result != null)
            {
                // Por cada valor del enum VirtualPointOfCare, creamos un PointOfCare asociado a la unidad
                foreach (var pocEnum in Enum.GetValues(typeof(VirtualPointOfCare)).Cast<VirtualPointOfCare>())
                {
                    var poc = new PointOfCare
                    {
                        UnitId = result.Id,
                        Status = StatusEnum.PointOfCare.Available,
                        Room = pocEnum.ToString(),
                        Bed = pocEnum.ToString()
                    };
                    var insertedPoc = await pocService.InsertPointOfCare(poc);
                    if (insertedPoc != null)
                    {
                        result.PointOfCareIds ??= [];
                        result.PointOfCareIds.Add(insertedPoc.Id);
                    }
                }
    
                // Actualizamos la unidad con los nuevos PointOfCareIds
                await unitService.UpdateUnit(result);
            }
    
            return result;
        }


    /// <summary>
    /// Asynchronously creates a <see cref="UnitInfoDto"/> for the specified unit, populated with counts of related entities such as admissions, discharges, displays, patients, points of care, and virtual points of care. Returns <c>null</c> if any of the underlying count operations fail, logging the error.
    /// </summary>
    /// <param name="unit">The unit for which to build the dependency information DTO.</param>
    /// <returns>A task that resolves to a <see cref="UnitInfoDto"/> containing the unit's dependency counts, or <c>null</c> if an error occurs while retrieving the counts.</returns>
    public async Task<UnitInfoDto?> GetUnitDependencyDto(Unit unit)
        {
            try
            {
                return new UnitInfoDto(unit)
                {
                    Admissions = await admissionService.CountAdmissionsByUnitId(unit.Id),
                    Discharges = await dischargeService.CountDischargesByUnitId(unit.Id),
                    Displays = await displayService.CountDisplaysByUnitId(unit.Id),
                    Patients = await patientService.CountPatientsByUnitId(unit.Id),
                    PointOfCares = await pocService.CountPoCsByUnitId(unit.Id),
                    VirtualPointOfCares = await pocService.CountVirtualPoCsByUnitId(unit.Id)
                };
            }
            catch (Exception e)
            {
                logger.LogError(e.Message);
                return null;
            }
        }

    /// <summary>
    /// Deletes a unit identified by its identifier, cascading the removal to all associated resources
    /// (admissions, discharges, authorizations, displays, and points of care). Throws a <see cref="NotFoundException"/>
    /// if the unit does not exist, and a <see cref="ConflictException"/> if the unit still has patients assigned to it.
    /// </summary>
    /// <param name="unitId">The identifier of the unit to delete.</param>
    /// <returns><c>true</c> if the unit and its related resources were successfully deleted; otherwise, <c>false</c> when an error is caught and logged.</returns>
    /// <exception cref="NotFoundException">Thrown when no unit is found for the specified <paramref name="unitId"/>.</exception>
    /// <exception cref="ConflictException">Thrown when the unit has patients assigned to it, preventing deletion.</exception>
    public async Task<bool> DeleteUnitById(ObjectId unitId)
    {
        try
        {
            var unit = await unitService.FindById(unitId);

            if (unit == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);

            //No se puede borrar una unidad que est� en uso 
            var patients = await patientService.CountPatientsByUnitId(unitId);
            if (patients > 0)
                //Si tiene pacientes no se permite borrado
                throw new ConflictException(HttpEnum.ErrorMessage.ConflictResourceInUse);

            //Borramos admisiones
            await admissionService.DeleteAdmissionsByUnitId(unitId);
            //discharges
            await dischargeService.DeleteDischargesByUnitId(unitId);
            //Borramos Authorizations
            await authService.DeleteByUnitId(unitId);

            //Displays 
            await displayService.DeleteDisplaysByUnitId(unitId);

            //PointOfCares
            await pocService.DeletePoCsByUnitId(unitId);


            //Una vez borrados los recursos asociados a la unidad, borramos la unidad
            await unitService.DeleteUnitById(unit);

            return true;
        }
        catch (Exception e)
        {
            Log.Error(e.Message);
            return false;
        }
    }

    #endregion

    #region Medicine

    /// <summary>
    /// Retrieves a medicine by its unique identifier, or throws an exception if the medicine cannot be found.
    /// </summary>
    /// <param name="medicineId">The unique identifier of the medicine to retrieve.</param>
    /// <returns>The medicine matching the specified identifier.</returns>
    /// <exception cref="NotFoundException">Thrown when no medicine is found with the specified identifier.</exception>
    public async Task<Medicine?> GetMedicineById(ObjectId medicineId)
        {
            return await medicineService.GetMedicineById(medicineId) ??
                   throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
        }

    /// <summary>
    /// Creates a new medicine by forwarding the request to the medicine service. Throws a conflict exception if the service is unable to create the medicine.
    /// </summary>
    /// <param name="medicine">The medicine to be created.</param>
    /// <returns>The created <see cref="Medicine"/>.</returns>
    /// <exception cref="ConflictException">Thrown when the medicine service fails to create the medicine.</exception>
    public async Task<Medicine?> PostMedicine(Medicine medicine)
        {
            var newMedicine = await medicineService.PostMedicine(medicine) ??
                              throw new ConflictException(HttpEnum.ErrorMessage.ConflictCreationFailed);
            return newMedicine;
        }

    /// <summary>
    /// Updates an existing <see cref="Medicine"/> by delegating to the medicine service.
    /// Throws a <see cref="ConflictException"/> when the service returns a null result, indicating the update could not be applied.
    /// </summary>
    /// <param name="medicine">The medicine entity containing the updated information to persist.</param>
    /// <returns>The updated <see cref="Medicine"/> returned by the service.</returns>
    /// <exception cref="ConflictException">Thrown when the update operation fails and the service returns a null result.</exception>
    public async Task<Medicine?> UpdateMedicine(Medicine medicine)
        {
            var updatedMedicine = await medicineService.UpdateMedicine(medicine) ??
                                  throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
            return updatedMedicine;
        }

    /// <summary>
    /// Deletes a medicine record by its identifier, validating the identifier format and confirming successful removal.
    /// </summary>
    /// <param name="medicineId">The string representation of the medicine's ObjectId to delete.</param>
    /// <returns>A task that resolves to <c>true</c> when the medicine has been successfully deleted.</returns>
    /// <exception cref="BadRequestException">Thrown when <paramref name="medicineId"/> is not a valid ObjectId format.</exception>
    /// <exception cref="ConflictException">Thrown when the medicine still exists after the delete operation, indicating the deletion failed.</exception>
    public async Task<bool> DeleteMedicineById(string medicineId)
        {
            if (!ObjectId.TryParse(medicineId, out var objectId))
                throw new BadRequestException(HttpEnum.ErrorMessage.BadRequestInvalidFormat);
            await medicineService.DeleteMedicineById(objectId);
            _ = await medicineService.GetMedicineById(ObjectId.Parse(medicineId)) ??
                throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
            return true;
        }

    #endregion
}