using System.Collections.Concurrent;
using adas_core.Application.Exceptions;
using adas_core.Application.Repositories.Interfaces;
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.Filter;
using adas_core.Domain.Models.GroupedObservations;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Pumps;
using adas_core.Domain.Models.Responses;
using adas_core.Domain.Utils;
using Microsoft.AspNetCore.Http;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;

namespace adas_core.Application.Services;

public class ConfigObservationService : IConfigObservationService
{
    private static readonly ConcurrentDictionary<ObjectId, ConfigObservationCached> CachedConfigObservations = new();

    private static readonly ConcurrentDictionary<string, ConfigObservationKeyCached>
        CachedConfigObservationKeys = new();

    private readonly IOptions<ApiSettings> _apiSettings;
    private readonly ILocalAuditService _auditService;
    private readonly IConfigObservationRepository _configObservationRepository;
    private readonly RetentionPolicy _defaultRetentionPolicy;
    private readonly int _defaultRetentionPolicyValue;
    private readonly IHttpContextAccessor _httpContextAccessor;
    private readonly bool _ignoreUnknownTreatment;
    private readonly ILogger<ConfigObservationService> _logger;
    private readonly int? _refreshTimeout;
    private readonly IUnitService _unitService;
    private readonly ICacheService _cacheService;
    private readonly CacheSettings? _cacheSettings; 
    
    private bool IgnoreUnknownObservation =>
        _apiSettings.Value.ConfigObservation?.IgnoreUnknownObservation ?? false;

    public ConfigObservationService(
        IConfigObservationRepository configObservationRepository,
        IOptions<ApiSettings> apiSettings,
        IOptions<CacheSettings> cacheSettings,
        ILogger<ConfigObservationService> logger,
        IUnitService unitService,
        IHttpContextAccessor httpContextAccessor,
        ILocalAuditService auditService,
        ICacheService cacheService
    )
    {
        _cacheService = cacheService;
        _cacheSettings = cacheSettings.Value;
        _configObservationRepository = configObservationRepository;
        _apiSettings = apiSettings;
        _logger = logger;
        _unitService = unitService;

        _refreshTimeout = _apiSettings.Value.ConfigObservation?.Refresh;
        _ignoreUnknownTreatment = _apiSettings.Value.ConfigObservation?.IgnoreUnknownTreatment ?? false;

        _defaultRetentionPolicyValue = _apiSettings.Value.RetentionPolicyValue;

        _defaultRetentionPolicy = RetentionPolicy.NoDelete;

        _httpContextAccessor = httpContextAccessor;
        _auditService = auditService;
    }

    public async Task<ICollection<ConfigObservation>> GetAllConfigs(CancellationToken ct = default)
    {
        
        var (key, ttl) = CacheKeys.ConfigObservationsAllKeyWithTtl(_cacheSettings);

        var result = await _cacheService.GetOrSetObjectAsync(
            key,
            async () => await _configObservationRepository.FindAll(),
            ttl,
            ct);

        return result;

    }

    public async Task<ConfigObservationDto> GetAllCompact()
    {
        var count = await _configObservationRepository.Count();
        return new ConfigObservationDto { ItemCount = count };
    }

    public async Task<PaginationResponse<ConfigObservation>> GetPaginatedItems(PaginationFilter filter)
    {
        var result = await _configObservationRepository.GetPaginatedItems(filter);
        var count = await _configObservationRepository.Count();
        return new PaginationResponse<ConfigObservation>(result.ToList(), filter.PageNumber, filter.PageSize,
            count);
    }


    public async Task<ConfigObservation?> GetConfigById(ObjectId id)
    {
        return await _configObservationRepository.FindById(id) ?? null;
    }

    public async Task<List<string>> GetConfigNames(string id)
    {
        return await _configObservationRepository.GetConfigNames(id);
    }

    public async Task<List<string>> GetConfigNames()
    {
        return await _configObservationRepository.GetConfigNames();
    }

    
    public async Task<ObservatitonRetentionResult?> RetentionActions<T>(T obs) where T : BasePatientObservation
    {
        var conf = await Get(obs);
        return conf is { RetentionPolicy: not null } ? 
            new ObservatitonRetentionResult(conf.RetentionPolicy.Value, conf.RetentionPolicyValue) : 
            new ObservatitonRetentionResult(RetentionPolicy.NoDelete, null);
    }

    

    public async Task<StatusEnum.Type> GroupedObservationStatus(GroupedField groupedField,
        GroupedObservationEnum.Result result,
        string name, object value, double? min, double? max)
    {
        var conf = await Get(name);
        if (conf == null) return StatusEnum.Type.Ok;
        PatientObservation? mapObs;
        if (conf.Grouped != null && groupedField.Group != null &&
            conf.Grouped.TryGetValue(groupedField.Group, out var grp))
        {
            mapObs = await MapConf(new PatientObservation { Name = name, Value = value, Min = min, Max = max }, grp);
            if (mapObs != null) return mapObs.Status;
        }

        if (conf.Grouped != null && conf.Grouped.ContainsKey(result.ToString()))
        {
            mapObs = await MapConf(new PatientObservation { Name = name, Value = value, Min = min, Max = max },
                conf.Grouped[result.ToString()]);
            if (mapObs != null) return mapObs.Status;
        }

        mapObs = await MapConf(new PatientObservation { Name = name, Value = value, Min = min, Max = max }, conf);
        if (mapObs != null) return mapObs.Status;

        return StatusEnum.Type.Ok;
    }

    public async Task<T?> Map<T>(T obs, bool onlyByName = false) where T : BasePatientObservation
    {
        var conf = onlyByName ? await Get(obs, onlyByName) : await Get(obs);
        if (conf == null)
        {
            _logger.LogDebug("Ignore Unknown Observation. {Name} {Code} {CodingSystem}", obs.Name, obs.Code,
                obs.CodingSystem);
            return IgnoreUnknownObservation ? null : obs;
        }

        if (string.IsNullOrEmpty(conf.Name))
        {
            _logger.LogError("Config Name is null or empty. {conf}", conf);
            return null;
        }

        return await MapConf(obs, conf);
    }

    public async Task<ConfigObservation?> RemoveConfigItem(ObjectId id)
    {
        var item = await _configObservationRepository.FindById(id);
        if (item == null) return null;
        
        var deleted = await _configObservationRepository.Delete(id);

        // Invalidar CACHE (colección completa)
        await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations));

        return deleted;

    }

    public async Task<PatientTreatment?> Map(PatientTreatment treatment)
    {
        ConfigObservation? conf = null;

        foreach (var requestGiveCode in treatment.RequestedGiveCodes)
            conf = string.IsNullOrEmpty(requestGiveCode.CodingSystem) &&
                   string.IsNullOrEmpty(requestGiveCode.Identifier)
                ? await Get(requestGiveCode.Text)
                : await GetByCodeSysAndCode(requestGiveCode.CodingSystem, requestGiveCode.Identifier);
        if (conf == null) return _ignoreUnknownTreatment ? null : treatment;

        return conf.Name == null ? null : treatment;
    }

    public async Task<ConfigObservation?> Get<T>(T obs, bool onlyByName = false)
        where T : BasePatientObservation
    {
        var items = await GetAllConfigs();
        if (items.Count == 0) return null;

        if (onlyByName)
        {
            var configItem = items.FirstOrDefault(i => i.Name == obs.Name);
            if (configItem != null) return await Process(configItem);
            _logger.LogError("Error getting Config observation Item. Observation: {obs}", obs);
            return null;

        }

        ConfigObservation? item = null;
        if ((!string.IsNullOrEmpty(obs.Code) && !string.IsNullOrEmpty(obs.CodingSystem)) ||
            obs.ParentData is { Code: not null, CodingSystem: not null })
            foreach (var obsConfig in items)
            {
                if (obs.Code != null && obs.Code != obsConfig.Code)
                    continue;
                if (obsConfig.CodingSystem != null && obs.CodingSystem != obsConfig.CodingSystem)
                    continue;
                if (obsConfig.OriginalName != null && obs.Name != obsConfig.OriginalName)
                    continue;
                if (obsConfig.ParentCode != null && obs.ParentData?.Code != obsConfig.ParentCode)
                    continue;
                if (obsConfig.ParentCodingSystem != null &&
                    obs.ParentData?.CodingSystem != obsConfig.ParentCodingSystem)
                    continue;
                if (obsConfig.ParentName != null && obs.ParentData?.Name != obsConfig.ParentName)
                    continue;

                if (obsConfig.OriginalName == null && obsConfig.Name != null && obs.Name != null &&
                    obs.Name.Contains("Alarm") && obs.Name != obsConfig.Name)
                    continue;

                if (obsConfig.Code == null && obsConfig.CodingSystem == null && obsConfig.ParentCode == null &&
                    obsConfig.ParentCodingSystem == null && obs.Name != null && !obs.Name.Contains("Alarm"))
                    continue;
                if (obsConfig.Code == null &&
                    obsConfig is { CodingSystem: not null, ParentCode: null, ParentCodingSystem: null } &&
                    obs.Name != null && !obs.Name.Contains("Alarm"))
                    continue;
                if (obsConfig.Code == null && obsConfig.CodingSystem == null && obsConfig.ParentCode == null &&
                    obsConfig.ParentCodingSystem != null && obs.Name != null && !obs.Name.Contains("Alarm"))
                    continue;
                if (obsConfig.Code == null &&
                    obsConfig is { CodingSystem: not null, ParentCode: null, ParentCodingSystem: not null } &&
                    obs.Name != null && !obs.Name.Contains("Alarm"))
                    continue;

                item = obsConfig;
                break;
            }
        else item = items.FirstOrDefault(i => i.Name == obs.Name);

        return item != null ? await Process(item) : null;
    }

    // public async Task<List<ConfigObservation>> GetAlarmWithRecordingConfig(ObjectId patientId)
    // {
    //     var configObservationId = await GetConfigObservationKeyFromPatientId(patientId);
    //     var configObservation = await GetConfig(configObservationId);
    //     return configObservation?.Items
    //         .Where(i => i is { CodingSystem: "ADAS_ALARM", Alarm.Recording.Enabled: true })
    //         .ToList() ?? [];
    // }

    public async Task<ConfigObservation?> GetByCodeSysAndCode(string? codingSystem, string? code)
    {
        var filteredResult = await _configObservationRepository.GetByCodeSysAndCode(codingSystem, code);
        if (filteredResult != null) return await Process(filteredResult);
        _logger.LogWarning("Config observation item not found. CodingSystem: {codingSystem} code: {code} ",
            codingSystem ?? "null", code ?? "null");
        return null;

    }

    public async Task<ConfigObservation?> Get(string? name)
    {
        if (string.IsNullOrEmpty(name)) return null;
        var result = await GetConfigByName(name);
        if (result == null)
        {
            // _logger.LogWarning(
            //     "Config observation item not found. name: {name} configObservationId: {configObservationId} ",
            //     name, name);
            return null;
        }

        return await Process(result);
    }

    public async Task<ConfigObservation?> UpdateConfig(ConfigObservation configObservationItem)
    {
        // if (!configObservationItem.Id.HasValue)
        //     return await _configObservationRepository.InsertOneAsyncAndReturn(configObservationItem);
        var configObservation = await _configObservationRepository.FindById(configObservationItem.Id) ??
                                throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);

        var updatedConfig = await _configObservationRepository.Update(configObservation);
        
        // Invalidar CACHE (colección completa)
        await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations));
        
        await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, configObservation,
            updatedConfig!);
        return updatedConfig;
    }

    public async Task<IEnumerable<ConfigObservation>?> GetConfigObservationItem(string name)
    {
        var configObservationItems = await _configObservationRepository.FindAllByName(name);
        if (configObservationItems.Count != 0)
            return configObservationItems;
        _logger.LogWarning("Cant get config item, config not found, name: {name} ", name);
        throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
    }

    public async Task<ConfigObservation?> GetSingleConfigObservationItem(string? code, string? codingSystem,
        string? name, string? originalName)
    {
        var matchingItem =
            await _configObservationRepository.GetSingleConfigObservationItem(code, codingSystem, name, originalName);

        if (matchingItem == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundNoMatches);

        return matchingItem;
    }

    public async Task<bool> DeleteSingleConfigObservationItem(ConfigObservation configObservationItem)
    {
        var configObservation = await _configObservationRepository.FindById(configObservationItem.Id) ??
                                throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);

        var auxConfigObservation = await _auditService.DeepCopyAsync(configObservation);

        _ = await _configObservationRepository.DeleteAsync(configObservation.Id) ??
            throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
        
        // Invalidar CACHE (colección completa)
        await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations));
        
        await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxConfigObservation,
            configObservation);
        return true;
    }

    public async Task<IEnumerable<ConfigObservation>> GetConfigObservationItemsByName(string name)
    {
        return await _configObservationRepository.FindAllByName(name);
    }

    public async Task<ConfigObservation?> CreateConfig(ConfigObservation configObservation)
    {
        
        var existing = await _configObservationRepository.FindById(configObservation.Id);
        if (existing != null)
            throw new BadRequestException(HttpEnum.ErrorMessage.ConflictCreationFailed);

        await _configObservationRepository.InsertOneAsyncAndReturn(configObservation);
        await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, null, configObservation);
        return configObservation;
    }


    public async Task<ConfigObservation?> RemoveConfigItem(string itemName)
    {
        var configObservation = await GetConfigByName(itemName) ??
                                throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
        var auxConfigObservation = await GetConfigByName(itemName) ??
                                   throw new ConflictException(HttpEnum.ErrorMessage.ConflictDeleteFailed);
        await _auditService.CreateAuditLogAsync(_httpContextAccessor.HttpContext?.User!, auxConfigObservation,
            configObservation);

        var result =  await _configObservationRepository.Delete(configObservation.Id!);
        
        // Invalidar CACHE (colección completa)
        await _cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.ConfigObservations));
        
        return result;
    }

    public async Task<ConfigObservation?> GetConfig(ObjectId configObservationId)
    {
        RefreshCachedConfigObservations();

        if (CachedConfigObservations.TryGetValue(configObservationId, out var cached)
            && DateTime.Now <= cached.NextRefresh)
            return cached.ConfigObservation;

        var config = await _configObservationRepository.FindById(configObservationId);
        cached = new ConfigObservationCached
        {
            ConfigObservation = config ?? new ConfigObservation(),
            NextRefresh = _refreshTimeout.HasValue ? DateTime.Now.AddSeconds(_refreshTimeout.Value) : DateTime.MinValue
        };

        CachedConfigObservations[configObservationId] = cached;

        return cached.ConfigObservation;
    }

    public async Task<ConfigObservation?> GetConfigByName(string name)
    {
        if (string.IsNullOrWhiteSpace(name)) return null;

        RefreshCachedConfigObservations();

        var cachedItem = CachedConfigObservations.Values
            .FirstOrDefault(cached =>
                DateTime.Now <= cached.NextRefresh &&
                cached.ConfigObservation is { Name: not null } &&
                cached.ConfigObservation.Name.Equals(name, StringComparison.OrdinalIgnoreCase));

        if (cachedItem != null) return cachedItem.ConfigObservation;
        var config = await _configObservationRepository.FindByName(name);
        if (config == null) return null;
        var newCachedItem = new ConfigObservationCached
        {
            ConfigObservation = config,
            NextRefresh = _refreshTimeout.HasValue
                ? DateTime.Now.AddSeconds(_refreshTimeout.Value)
                : DateTime.MinValue
        };

        CachedConfigObservations[config.Id!] = newCachedItem;
        
        return config;
    }

    private void RefreshCachedConfigObservations()
    {
        if (!_refreshTimeout.HasValue) return;

        // Use ConcurrentDictionary's thread-safe features to identify and remove expired keys
        var keysToRemove = CachedConfigObservations
            .Where(k => DateTime.Now > k.Value.NextRefresh)
            .Select(k => k.Key)
            .ToList();

        foreach (var key in keysToRemove) CachedConfigObservations.TryRemove(key, out _);
    }

    private Task<ConfigObservation> Process(ConfigObservation confItem)
    {
        confItem.RetentionPolicy ??= _defaultRetentionPolicy;
        if (confItem.RetentionPolicy != RetentionPolicy.NoDelete && !confItem.RetentionPolicyValue.HasValue)
        {
            confItem.RetentionPolicyValue = _defaultRetentionPolicyValue;
            if (confItem.RetentionPolicyValue <= 0) confItem.RetentionPolicyValue = null;
        }
        else
        {
            confItem.RetentionPolicyValue = null;
        }

        return Task.FromResult(confItem);
    }

    private void RefreshCachedConfigObservationKeys()
    {
        try
        {
            if (!_refreshTimeout.HasValue) return;

            // Use ConcurrentDictionary's thread-safe features to identify and remove expired keys
            var keysToRemove = CachedConfigObservationKeys
                .Where(k => DateTime.Now > k.Value.NextRefresh)
                .Select(k => k.Key)
                .ToList();

            foreach (var key in keysToRemove) CachedConfigObservationKeys.TryRemove(key, out _);
        }
        catch (Exception ex)
        {
            _logger.LogError("Error refreshing cached config observation keys. Exception: {ex}", ex);
        }
    }

    private async Task<T?> MapConf<T>(T obs, ConfigObservation conf) where T : BasePatientObservation
    {
        _logger.LogTrace("Mapping observation: {obs}", obs);

        obs.Name = conf.Name;
        if (obs is PatientObservation)
        {
            var noCalculateStatusWithCodingSystem = _apiSettings.Value.NoCalculateStatusWithCodingSystem;
            if (noCalculateStatusWithCodingSystem != null &&
                obs.CodingSystem == noCalculateStatusWithCodingSystem) return obs;
            if (obs is not PatientObservation pobs)
                return obs;

            if (conf.MaxAlert != null && (conf.ForceAlert || !pobs.Max.HasValue)) pobs.Max = conf.MaxAlert;

            if (conf.MinAlert.HasValue && (conf.ForceAlert || !pobs.Min.HasValue)) pobs.Min = conf.MinAlert;

            if (conf.MaxWarn.HasValue && (conf.ForceWarn || !pobs.MaxWarn.HasValue)) pobs.MaxWarn = conf.MaxWarn;

            if (conf.MinWarn.HasValue && (conf.ForceWarn || !pobs.MinWarn.HasValue)) pobs.MinWarn = conf.MinWarn;

            if (conf.LevelCondition != null)
                try
                {
                    var evalCondition = await CSharpScript.EvaluateAsync(conf.LevelCondition, globals: pobs);
                    if (int.TryParse(evalCondition.ToString(), out var evalInt))
                        pobs.Level = evalInt;
                    else
                        _logger.LogError("Error evaluating condition: {evalCondition} for obs: {pobs}",
                            evalCondition, pobs);
                }
                catch (CompilationErrorException e)
                {
                    _logger.LogError("Error evaluating expression for obs: {obs} error: {diagnostics}", obs,
                        e.Diagnostics);
                }
                catch (Exception e)
                {
                    _logger.LogError("Error evaluating expression for obs: {obs} error: {e}", obs, e);
                }

            pobs.Status = StatusEnum.Type.Ok;
            if (pobs.Value.IsNumber())
            {
                if (pobs.Min.HasValue && pobs.Min > pobs.Value.ToDouble() &&
                    conf.Alert is null or '<')
                {
                    pobs.Status = StatusEnum.Type.Alert;
                    pobs.AlertColor = conf.AlertColor;
                }
                else if (pobs.Max.HasValue && pobs.Max < pobs.Value.ToDouble() &&
                         conf.Alert is null or '>')
                {
                    pobs.Status = StatusEnum.Type.Alert;
                    pobs.AlertColor = conf.AlertColor;
                }
                else if (pobs.MinWarn.HasValue && pobs.MinWarn > pobs.Value.ToDouble() &&
                         conf.Alert is null or '<')
                {
                    pobs.Status = StatusEnum.Type.Warning;
                    pobs.WarnColor = conf.WarnColor;
                }
                else if (pobs.MaxWarn.HasValue && pobs.MaxWarn < pobs.Value.ToDouble() &&
                         conf.Alert is null or '>')
                {
                    pobs.Status = StatusEnum.Type.Warning;
                    pobs.WarnColor = conf.WarnColor;
                }
            }
            else if (pobs.Value is string)
            {
                if (conf.AlertValues != null && conf.AlertValues.Any() && conf.AlertValues.Contains(pobs.Value))
                    pobs.Status = StatusEnum.Type.Alert;
                else if (conf.WarningValues != null && conf.WarningValues.Any() &&
                         conf.WarningValues.Contains(pobs.Value)) pobs.Status = StatusEnum.Type.Warning;
            }

            if (conf.Expires is > 0)
            {
                pobs.Expires = conf.Expires;
                //check if already is expired
                var expireTime = obs.Time.ToLocalTime().AddSeconds(Convert.ToDouble(conf.Expires));
                pobs.Expired = DateTime.Now.CompareTo(expireTime) > 0;
            }

            pobs.ShowOnExpired = conf.ShowOnExpired;

            if (conf.ColorOnExpired != null) pobs.ColorOnExpired = conf.ColorOnExpired;

            if (conf.Persist != null)
                pobs.Persist = conf.Persist;

            if (conf.UiConfiguration != null && conf.UiConfiguration.Any()) pobs.UiConfiguration = conf.UiConfiguration;

            if (conf.Alarm != null) pobs.Alarm = conf.Alarm;

            if (conf.TimeFromMessageTime)
                if (pobs.MessageTime.CompareTo(DateTime.MinValue) != 0)
                    pobs.Time = pobs.MessageTime;

            if (pobs.Units == null || conf.ForceUnits) pobs.Units = conf.Units;

            if (conf.CreateObservation != null) pobs.CreateObservation = conf.CreateObservation;

            pobs.CheckObservations = conf.CheckObservations;
        }

        if (obs is PatientObservationAlarm)
        {
            if (obs is not PatientObservationAlarm pobs)
                return obs;

            if (conf.Expires is > 0)
            {
                pobs.Expires = conf.Expires;
                //check if already is expired
                var expireTime = obs.Time.ToLocalTime().AddSeconds(Convert.ToDouble(conf.Expires));
                pobs.Expired = DateTime.Now.CompareTo(expireTime) > 0;
            }

            if (conf.Persist != null)
                pobs.Persist = conf.Persist;

            if (conf.Alarm != null) pobs.AlarmConfig = conf.Alarm;

            if (conf.AlertColor != null) pobs.AlertColor = conf.AlertColor;

            if (conf.TimeFromMessageTime)
                if (pobs.MessageTime.CompareTo(DateTime.MinValue) != 0)
                    pobs.Time = pobs.MessageTime;

            if (pobs.Units == null || conf.ForceUnits) pobs.Units = conf.Units;

            if (conf.CreateObservation != null) pobs.CreateObservation = conf.CreateObservation;

            pobs.CheckObservations = conf.CheckObservations;
        }

        _logger.LogTrace("Observation mapped: {obs}", obs);

        return obs;
    }

    private class ConfigObservationCached
    {
        public DateTime NextRefresh { get; set; }
        public ConfigObservation? ConfigObservation { get; set; }
    }

    private class ConfigObservationKeyCached
    {
        public DateTime NextRefresh { get; set; }
        public string? Key { get; set; }
    }
}