using adas_core.Application.Exceptions;
using adas_core.Application.Repositories.Interfaces;
using adas_core.Application.Services.Interfaces;
using adas_core.Application.Subscriptions;
using adas_core.Domain.Enums;
using adas_core.Domain.Models;
using adas_core.Domain.Models.AppSettings;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;

namespace adas_core.Application.Services;

/// <summary>
/// Provides the implementation of the <see cref="IDiagnosisService"/> contract,
/// offering diagnosis-related operations as defined by the interface.
/// </summary>
public class DiagnosisService : IDiagnosisService
{
    private readonly ILocalAuditService _auditService;
    private readonly Lazy<ICalculatedObservationsService> _calculatedObservations;
    private readonly IClientMessageService _clientMessageService;
    private readonly IDiagnosisArchiveRepository _diagnosisArchiveRepository;

    private readonly List<string> _diagnosisCode = [];
    private readonly IDiagnosisRepository _diagnosisRepository;

    private readonly string _diagnosisSystem;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly ILogger<DiagnosisService> _logger;
    private readonly Lazy<IPatientService> _patientService;
    private readonly ISubscribersService _subscribersService;
    private readonly IUnitService _unitService;


    public DiagnosisService(
        Lazy<IPatientService> patientService,
        IOptions<ApiSettings> apiSettings,
        IDiagnosisRepository diagnosisRepository,
        IDiagnosisArchiveRepository diagnosisArchiveRepository,
        ILogger<DiagnosisService> logger,
        IClientMessageService clientMessageService,
        ISubscribersService subscribersService,
        Lazy<ICalculatedObservationsService> calculatedObservations,
        IHttpContextAccessor httpContextAccessor,
        ILocalAuditService auditService,
        IUnitService unitService)
    {
        _patientService = patientService;
        _diagnosisRepository = diagnosisRepository;
        _diagnosisArchiveRepository = diagnosisArchiveRepository;
        _logger = logger;
        _clientMessageService = clientMessageService;
        _subscribersService = subscribersService;
        _calculatedObservations = calculatedObservations;
        _unitService = unitService;
        _httpContextAccessor = httpContextAccessor;
        _auditService = auditService;


        _diagnosisSystem = apiSettings.Value.DiagnosisSystem ?? "CUSTOM";

        if (apiSettings.Value.DiagnosisCode.Any())
            _diagnosisCode = apiSettings.Value.DiagnosisCode;
    }

    // private async Task SendBroadcast(PatientDiagnosis diagnosis)
    // {
    //     var patient = await _patientService.Value.FindById(diagnosis.PatientId);
    //     if (patient == null) return;
    //
    //
    //     var subscribers = _subscribersService.GetSubscribers().Where(s =>
    //         (s.SubscriptionType == SubscriptionType.Box && s.Box == patient.Bed &&
    //          s.Section == patient.UnitString) ||
    //         (s.SubscriptionType == SubscriptionType.Section && s.Section == patient.UnitString)).ToList();
    //     subscribers.ForEach(Action);
    //     return;
    //
    //     async void Action(WsSubscriber subscriber) =>
    //         await _clientMessageService.SendAsync(subscriber.Id, OperationType.Diagnosis, diagnosis);
    // }

    /// <summary>
    /// Archives the specified patient by delegating to the archival routine keyed by the patient's identifier.
    /// </summary>
    /// <param name="patient">The patient to be archived. Its <c>Id</c> is used to locate the record to archive.</param>
    public async Task Archive(Patient patient)
        {
            await ArchiveByPatientId(patient.Id);
        }

    /// <summary>
    /// Archives all diagnoses associated with the specified patient by copying them to the diagnosis archive repository and then deleting the original records.
    /// </summary>
    /// <param name="id">The identifier of the patient whose diagnoses will be archived.</param>
    public async Task ArchiveByPatientId(ObjectId id)
        {
            _logger.LogDebug("Archive Diagnoses by Patient Id {id}", id);
            using (var cursor = await FindByPatientIdAsync(id))
            {
                while (await cursor.MoveNextAsync())
                    foreach (var current in cursor.Current)
                        await _diagnosisArchiveRepository.InsertOneAsync(current);
            }
    
            await DeleteByPatientId(id);
        }

    /// <summary>
    /// Deletes all diagnoses associated with the specified patient identifier and records an audit log entry capturing the previous state of the records.
    /// </summary>
    /// <param name="id">The unique identifier of the patient whose diagnoses should be deleted.</param>
    public async Task DeleteByPatientId(ObjectId id)
        {
            _logger.LogDebug("Delete Diagnoses by Patient Id {id}", id);
            var oldPatient = await _diagnosisRepository.GetByPatient(id);
            await _diagnosisRepository.DeleteByPatientId(id);
            await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, oldPatient, null);
        }

    /// <summary>
    /// Asynchronously retrieves the list of patient diagnoses associated with the specified patient identifier by delegating to the diagnosis repository.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient whose diagnoses are being retrieved.</param>
    /// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientDiagnosis"/> records for the given patient.</returns>
    public async Task<List<PatientDiagnosis>> GetByPatientId(ObjectId patientId)
        {
            var diagnosis = await _diagnosisRepository.GetByPatient(patientId);
    
            return diagnosis;
        }


    /// <summary>
    /// Asynchronously saves the specified API request by delegating to an overload that accepts a secondary parameter, which is passed as null.
    /// </summary>
    /// <param name="apiRequest">The API request to save.</param>
    /// <returns>A task that represents the asynchronous save operation.</returns>
    public Task SaveRequest(ApiRequest apiRequest)
        {
            return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest, null); }));
        }

    /// <summary>
    /// Asynchronously saves the specified API request by delegating to the underlying save operation with a null secondary parameter.
    /// </summary>
    /// <param name="apiRequest">The API request instance to persist.</param>
    /// <returns>A task that represents the asynchronous save operation.</returns>
    public Task SaveRequestAsync(ApiRequest apiRequest)
        {
            return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest, null); }));
        }

    /// <summary>
    /// Processes a diagnosis observation from an API request and persists it as a patient diagnosis. Maps SNOMED-coded observation values to diagnosis properties (description, label, code, state, category, start/end time), falling back to the current date when the observation time is missing, and skipping processing if the observations collection is null.
    /// </summary>
    /// <param name="apiRequest">The API request containing the observation codes, values, message time, and optional observation time used to build the diagnosis.</param>
    /// <param name="patient">The patient associated with the diagnosis, whose identifier is assigned to the new <see cref="PatientDiagnosis"/>.</param>
    /// <returns>A task that represents the asynchronous insertion of the resulting <see cref="PatientDiagnosis"/>.</returns>
    public async Task ProcessDiagnosisObservation(ApiRequest apiRequest, Patient patient)
        {
            _logger.LogDebug("INSERT DiagnosisObservation from obsservation");
    
            var time = apiRequest.ObservationData?.Time;
            if (time == null)
                _logger.LogWarning("Processing diagnosis without time. Set Datetime now {DateTimeNow}. ", DateTime.Now);
    
            var obs = new PatientDiagnosis
            {
                CodingSystem = _diagnosisSystem,
                Time = time ?? DateTime.Now,
                PatientId = patient.Id,
                MessageTime = apiRequest.MessageTime
            };
    
            if (apiRequest.Observations == null)
            {
                _logger.LogError("ApiRequest Observations null. ");
                return;
            }
    
            for (var i = 0; i <= apiRequest.Observations.Count - 1; i++)
            {
                var value = apiRequest.Observations[i].Value;
    
                var strValue = value.ToString() ?? "null";
    
                switch (apiRequest.Observations[i].Code)
                {
                    case "272099008":
                        obs.Description = strValue;
                        break;
    
                    case "1000000013":
                        obs.Label = strValue;
                        break;
    
                    case "1000000014":
                        obs.Code = strValue;
                        break;
    
                    case "394731006":
                        obs.State = strValue;
                        break;
    
                    case "272125009":
                        obs.Category = strValue;
                        break;
    
                    case "398201009":
                        if (DateTime.TryParse(apiRequest.Observations[i].Value.ToString(), out var startTime))
                            obs.StartTime = startTime;
                        break;
    
                    case "397898000":
                        if (DateTime.TryParse(apiRequest.Observations[i].Value.ToString(), out var endTime))
                            obs.EndTime = endTime;
                        break;
                }
            }
    
            await InsertDiagnosis(obs);
        }

    /// <summary>
    /// Processes an incoming API request for diagnosis-related observations, resolving the patient from the request when not supplied. Validates that at least one of the patient number or location unit name is provided, handles ORU_R01 and ORU_R40 observation messages, and triggers diagnosis observation processing when the observation code is recognized.
    /// </summary>
    /// <param name="apiRequest">The API request containing the type, patient identifiers, location, and observation data to be processed.</param>
    /// <param name="patient">An optional pre-resolved patient; when null, the patient is resolved via the patient service using the request data.</param>
    /// <exception cref="ApiRequestException">Thrown when both the patient number and the location unit name are missing from the request, or when the request type is not valid for diagnosis processing.</exception>
    public async Task SaveRequest(ApiRequest apiRequest, Patient? patient)
        {
            if (patient == null)
            {
                _logger.LogDebug("message:ApiRequest Diagnosis");
                if (string.IsNullOrEmpty(apiRequest.PatientNumber) &&
                    string.IsNullOrEmpty(apiRequest.Location?.UnitName))
                {
                    _logger.LogDebug("person and PointOfCare are nulls");
                    throw new ApiRequestException("person and PointOfCare are nulls");
                }
    
                _logger.LogDebug("patientNumber:  {apiRequestpatientNumber} location:  {apiRequestlocation}",
                    apiRequest.PatientNumber, apiRequest.Location);
                _logger.LogDebug("RequestType:  {apiRequesttype}", apiRequest.Type);
    
                patient = await _patientService.Value.FindPatientByApiRequest(apiRequest);
            }
    
            if (patient == null)
            {
                // NO PATIENTS OR LOCATIONS WERE FOUND 
                _logger.LogWarning(
                    "Observation for patient: {apiRequestpatientNumber} with location: {apiRequestlocation} not found, ignoring",
                    apiRequest.PatientNumber, apiRequest.Location);
                return;
            }
    
            var unitConfig = await _unitService.FindById(patient.UnitId);
            if (!Hl7Utils.ManageAutoAdt(unitConfig, null, _logger, "ORU Diagnosis")) return;
    
            switch (apiRequest.Type)
            {
                //* ORU_R01 - Unsolicited transmission of an observation message
                //* ORU_R40 - Unsolicited transmission of an alert observation message
    
                case "ORU_R01":
                case "ORU_R40":
    
                    // OBSERVATIONS
                    if (apiRequest.Observation != null && apiRequest.Observations?.FirstOrDefault() == null)
                        apiRequest.Observations = [apiRequest.Observation];
    
                    if (apiRequest.Observations != null)
                    {
                        var obrcode = apiRequest.ObservationData?.Code;
    
                        if (apiRequest.ObservationData?.Value != null)
                            apiRequest.Observations.Add(new PatientObservation
                                { Value = apiRequest.ObservationData.Value });
    
                        if (obrcode != null && _diagnosisCode.Contains(obrcode))
                            _ = ProcessDiagnosisObservation(apiRequest, patient);
                    }
    
    
                    break;
                default:
                    _logger.LogWarning("ApiRequest type {apiRequesttype} sis not valid for Diagnosis",
                        apiRequest.Type);
                    throw new ApiRequestException("ApiRequest type " + apiRequest.Type +
                                                  " is not valid for Diagnosis");
            }
        }

    /// <summary>
    /// Processes a list of patient diagnoses by associating each entry with the specified patient and message time, then inserting them as diagnosis observations.
    /// </summary>
    /// <param name="diagnosis">The list of patient diagnoses to be processed and inserted.</param>
    /// <param name="patient">The patient whose identifier is assigned to each diagnosis entry.</param>
    /// <param name="messageTime">The timestamp assigned to each diagnosis entry during processing.</param>
    public async Task ProcessDiagnosis(List<PatientDiagnosis> diagnosis, Patient patient, DateTime messageTime)
        {
            _logger.LogDebug("INSERT DiagnosisObservation from diagnosis");
    
            foreach (var d in diagnosis)
            {
                d.PatientId = patient.Id;
                d.Time = messageTime;
                await InsertDiagnosis(d);
            }
        }

    /// <summary>
    /// Updates many diagnosis records, replacing the <paramref name="oldId"/> with the new <paramref name="id"/> for the specified <paramref name="nameId"/> field, by delegating to the diagnosis repository.
    /// </summary>
    /// <param name="nameId">The name of the identifier field used to locate the records to update.</param>
    /// <param name="id">The new ObjectId to assign to the matching records.</param>
    /// <param name="oldId">The existing ObjectId to be replaced in the matching records.</param>
    public async Task UpdateManyObjectId(string nameId, ObjectId id, ObjectId oldId)
        {
            await _diagnosisRepository.UpdateManyObjectId(nameId, id, oldId);
        }

    /// <summary>
    /// Retrieves all diagnoses associated with the specified patient identifier from the diagnosis repository.
    /// </summary>
    /// <param name="id">The unique identifier of the patient whose diagnoses are being requested.</param>
    /// <returns>A task that represents the asynchronous operation, containing a list of <see cref="PatientDiagnosis"/> records for the given patient.</returns>
    public async Task<List<PatientDiagnosis>> GetByPatient(ObjectId id)
        {
            return await _diagnosisRepository.GetByPatient(id);
        }

    /// <summary>
    /// Inserts a patient diagnosis into the repository after mapping it to the underlying data model, and broadcasts the stored record.
    /// If the diagnosis cannot be mapped (returns null), the insert and broadcast operations are skipped.
    /// </summary>
    /// <param name="diagnosis">The patient diagnosis to insert.</param>
    public async Task Insert(PatientDiagnosis diagnosis)
        {
            _logger.LogDebug("Insert {diagnosis}", diagnosis);
            var diag = await MapDiagnosis(diagnosis);
            if (diag != null)
            {
                await _diagnosisRepository.InsertOneAsync(diag);
                await SendBroadcast(diag);
            }
        }

    /// <summary>
    /// Maps a <see cref="PatientDiagnosis"/> through the calculated observations mapper to produce a transformed diagnosis instance.
    /// When the mapper yields a <see langword="null"/> result, indicating the diagnosis is not applicable or cannot be mapped, a debug message is logged and the result is returned as-is.
    /// </summary>
    /// <param name="diagnosis">The patient diagnosis to be mapped.</param>
    /// <returns>The mapped <see cref="PatientDiagnosis"/> produced by the calculated observations mapper, or <see langword="null"/> if the diagnosis was ignored.</returns>
    private async Task<PatientDiagnosis?> MapDiagnosis(PatientDiagnosis diagnosis)
        {
            var diagnosis2 = await _calculatedObservations.Value.Map(diagnosis);
            if (diagnosis2 == null) _logger.LogDebug("Mapping {diagnosis}: Ignored", diagnosis2);
    
            return diagnosis2;
        }

    /// <summary>
    /// Sends a patient diagnosis broadcast to all subscribers whose registered location matches the patient's location (unit, room, and bed). Returns early without broadcasting if the patient cannot be found.
    /// </summary>
    /// <param name="diagnosis">The patient diagnosis payload to broadcast to the matching subscribers.</param>
    private async Task SendBroadcast(PatientDiagnosis diagnosis)
        {
            var patient = await _patientService.Value.FindById(diagnosis.PatientId);
            if (patient == null) return;
    
            var displaySubscribers = _subscribersService.GetSubscribers().Where(s =>
                !s.Locations.IsNullOrEmpty() && s.Locations.Any(c =>
                    c.UnitName == patient.Location.UnitName &&
                    c.Bed == patient.Location.Bed &&
                    c.Room == patient.Location.Room
                )).ToList();
    
            displaySubscribers.ForEach(Action);
            return;
    
            void Action(WsSubscriber subscriber)
            {
                _clientMessageService.SendAsync(subscriber.Id, OperationType.Diagnosis, diagnosis);
            }
        }

    /// <summary>
    /// Asynchronously retrieves all patient diagnosis records associated with the specified patient identifier by delegating to the diagnosis repository.
    /// </summary>
    /// <param name="patientId">The unique identifier of the patient whose diagnosis records are to be retrieved.</param>
    /// <returns>A task that represents the asynchronous operation, containing an asynchronous cursor over the matching <see cref="PatientDiagnosis"/> records.</returns>
    public Task<IAsyncCursor<PatientDiagnosis>> FindByPatientIdAsync(ObjectId patientId)
        {
            return _diagnosisRepository.FindByPatientIdAsync(patientId);
        }

    /// <summary>
    /// Inserts or updates a patient diagnosis, creating an audit log entry. If a diagnosis
    /// already exists for the same patient, code, and coding system, it is updated while
    /// preserving its identifier and original timestamp; otherwise a new record is inserted.
    /// A broadcast is dispatched asynchronously after a successful insert or update.
    /// </summary>
    /// <param name="diagnosis">The patient diagnosis to persist.</param>
    public async Task InsertDiagnosis(PatientDiagnosis diagnosis)
        {
            try
            {
                _logger.LogDebug("Insert {diagnosis}", diagnosis);
                var diag = await MapDiagnosis(diagnosis);
                if (diag == null)
                {
                    _logger.LogError("Mepped diagnosis is null. Diagnosis: {diagnosis}", diagnosis);
                }
                else
                {
                    var dgdb = await _diagnosisRepository.FindByPatientIdAndCode(diag.PatientId, diag.Code,
                        diag.CodingSystem);
    
                    if (dgdb != null)
                    {
                        var auxDgdb = dgdb;
                        diag.Id = dgdb.Id;
                        diag.Time = dgdb.Time;
                        diag.UpdateDate = diagnosis.Time;
                        await _diagnosisRepository.UpdateOneAsync(diag.Id, diag);
                        await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxDgdb, dgdb);
                    }
                    else
                    {
                        await _diagnosisRepository.InsertOneAsync(diagnosis);
                        await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, diagnosis);
                    }
    
                    _ = SendBroadcast(diagnosis);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError("Error inserting diagnosis. Diagnosis: {diagnosis}. Exception:{ex}", diagnosis, ex);
            }
        }
}