using System.Text.RegularExpressions;
using MongoDB.Bson;
using adas_core.Domain.Enums;
using adas_core.Domain.Models.AppSettings;

namespace adas_core.Domain.Utils
{
    
    //  TTL RESOLVER POR ENTIDAD / CLAVE
    /// <summary>
    /// Resuelve TTLs a partir de CacheSettings teniendo en cuenta:
    ///  - El backend seleccionado por entidad (None | Cache | Redis)
    ///  - El TTL de la entidad para ese backend
    ///  - Caída a GlobalSeconds del backend si no hay TTL específico
    ///  - Si el TTL resultante es null o menor o igual a 0 sin expiración
    /// </summary>
    public static class CacheKeyTtl
    {
        private static TimeSpan? SecondsOrNull(int? seconds)
            => seconds is > 0
                ? TimeSpan.FromSeconds(seconds.Value)
                : null;

        private static int? FromInMemory(Func<TtlSettings, int?> selector, CacheSettings s)
            => selector(s.InMemory.Ttl);

        private static int? FromRedis(Func<TtlSettings, int?>? selector, CacheSettings? s)
            => selector?.Invoke(s?.Redis.Ttl ?? new TtlSettings());

        private static TimeSpan? GlobalInMemory(CacheSettings? s)
            => SecondsOrNull(s?.InMemory.Ttl.GlobalSeconds);

        private static TimeSpan? GlobalRedis(CacheSettings? s)
            => SecondsOrNull(s?.Redis.Ttl.GlobalSeconds);

        /// <summary>
        /// Resuelve TTL para una entidad concreta, respetando el backend configurado para dicha entidad.
        /// </summary>
        public static TimeSpan? ResolveForEntity(CacheSettings? settings, CacheEnum.EntityType entity)
        {
            // Selección del backend según la entidad
            if (settings == null) return null;
            
            var mode = entity switch
            {
                CacheEnum.EntityType.Patients            => settings.Patients,
                CacheEnum.EntityType.Displays            => settings.Displays,
                CacheEnum.EntityType.PumpObservations    => settings.PumpObservations,
                CacheEnum.EntityType.Appointments        => settings.Appointments,
                CacheEnum.EntityType.PontOfCare          => settings.PointOfCares,
                CacheEnum.EntityType.GroupedObservations => settings.GroupedObservations,
                CacheEnum.EntityType.PatientObservations => settings.PatientObservations,
                _                                        => CacheEnum.Mode.Cache
            };

            if (!settings.IsEnabled || mode == CacheEnum.Mode.None)
                return null;

            // Selector de TTL específico por entidad
            Func<TtlSettings, int?> selector = entity switch
            {
                CacheEnum.EntityType.Patients            => x => x.PatientsSeconds,
                CacheEnum.EntityType.Displays            => x => x.DisplaysSeconds,
                CacheEnum.EntityType.PumpObservations    => x => x.PumpObservationsSeconds,
                CacheEnum.EntityType.Appointments        => x => x.AppointmentsSeconds,
                CacheEnum.EntityType.PontOfCare          => x => x.PointOfCaresSeconds,
                CacheEnum.EntityType.GroupedObservations => x => x.GroupedObservationsSeconds,
                CacheEnum.EntityType.PatientObservations => x => x.PatientObservationsSeconds,
                CacheEnum.EntityType.ConfigObservations  => x => x.ConfigObservationsSeconds,
                _                                        => x => x.GlobalSeconds
            };

            // Resolver en función del backend elegido para la entidad
            return mode switch
            {
                CacheEnum.Mode.Cache => SecondsOrNull(FromInMemory(selector, settings))
                                        ?? GlobalInMemory(settings),

                CacheEnum.Mode.Redis => SecondsOrNull(FromRedis(selector, settings))
                                        ?? GlobalRedis(settings),

                _ => null
            };
            
        }

        /// <summary>
        /// Dada una clave, clasifica la entidad y resuelve el TTL para esa clave.
        /// </summary>
        public static TimeSpan? ResolveForKey(CacheSettings settings, string key)
        {
            var entity = CacheKeyClassifier.Classify(key);
            return ResolveForEntity(settings, entity);
        }
    }

    
    //  CACHE KEYS (GENERATION)
    /// <summary>
    /// Generador centralizado de claves de caché.
    /// - Prefijos normalizados para que el CacheDispatcher clasifique el backend.
    /// - Overloads "KeyWithTtl" para devolver (key, ttl) en una llamada.
    /// </summary>
    public static class CacheKeys
    {
        #region ConfigObservations
        
        public static string ConfigObservationsAll()
            => "configObservations:all";

        public static (string Key, TimeSpan? Ttl) ConfigObservationsAllKeyWithTtl(
            CacheSettings? settings)
        {
            // lo cual usará CacheEnum.EntityType.ConfigObservations (debes añadirlo)
            var key = ConfigObservationsAll();
            var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.ConfigObservations);
            return (key, ttl);
        }
        #endregion
        
        #region DisplayConfig
        public static string DisplayBase(ObjectId displayId)
            => $"configDisplays:display:{displayId}:base";
        
        public static (string Key, TimeSpan? Ttl) DisplayBaseKeyWithTtl(
            CacheSettings? settings,
            ObjectId displayId)
        {
            var key = DisplayBase(displayId);
            var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Displays);
            return (key, ttl);
        }
        
        public static string DisplayWithConfig(ObjectId displayId)
            => $"configDisplays:display:{displayId}:config";

        public static (string Key, TimeSpan? Ttl) DisplayWithConfigKeyWithTtl(
            CacheSettings? settings,
            ObjectId displayId)
        {
            var key = DisplayWithConfig(displayId);
            var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Displays);
            return (key, ttl);
        }
        
        #endregion
        
        #region PointOfCare
        public static string PointOfCareBase(ObjectId pocId)
            => $"pointOfCare:{pocId}:base";
        
        public static (string Key, TimeSpan? Ttl) PointOfCareBaseKeyWithTtl(
            CacheSettings? settings, ObjectId pocId)
        {
            var key = PointOfCareBase(pocId);
            var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare);
            return (key, ttl);
        }

        public static string PointOfCareWithInfoKeyWithTtl(ObjectId pocId)
            => $"pointOfCare:{pocId}:withInfo";

        public static (string Key, TimeSpan? Ttl) PointOfCareWithInfoKeyWithTtl(
            CacheSettings? settings, ObjectId pocId)
        {
            var key = PointOfCareWithInfoKeyWithTtl(pocId);
            var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare);
            return (key, ttl);
        }
        
        #endregion
        
        #region Observations
        public static string LatestObservations(ObjectId patientId, IEnumerable<string> fieldNames, int? last = null)
            => $"patients:latestObs:{patientId}:{Normalize(fieldNames)}:{last ?? 0}";

        public static (string Key, TimeSpan? Ttl) LatestObservationsKeyWithTtl(
            CacheSettings? settings, ObjectId patientId, IEnumerable<string> fieldNames, int? last = null)
        {
            var key = LatestObservations(patientId, fieldNames, last);
            var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PatientObservations);
            return (key, ttl);
        }

        #endregion
        
        #region PumpObservations
        public static string LatestPumps(ObjectId patientId, int? last = null)
            => $"pumpObs:latest:{patientId}:{last ?? 0}";

        public static (string Key, TimeSpan? Ttl) LatestPumpsKeyWithTtl(
            CacheSettings settings, ObjectId patientId, int? last = null)
        {
            var key = LatestPumps(patientId, last);
            var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PumpObservations);
            return (key, ttl);
        }
        
        #endregion

        #region  Appointments
        public static string PatientAppointmentsToday(ObjectId patientId)
        {
            var dateKey = DateTime.UtcNow.ToString("yyyyMMdd");
            return $"appointments:patient:{patientId}:{dateKey}";
        }

        public static (string Key, TimeSpan? Ttl) PatientAppointmentsTodayKeyWithTtl(
            CacheSettings? settings,
            ObjectId patientId)
        {
            var key = PatientAppointmentsToday(patientId);
            var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.Appointments);
            return (key, ttl);
        }
        
        public static string PocAppointmentsToday(ObjectId pocId)
        {
            var dateKey = DateTime.UtcNow.ToString("yyyyMMdd");
            return $"appointments:PoC:{pocId}:{dateKey}";
        }

        public static (string Key, TimeSpan? Ttl) PocAppointmentsTodayKeyWithTtl(
            CacheSettings? settings,
            ObjectId pocId)
        {
            var key = PocAppointmentsToday(pocId);
            var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.PontOfCare);
            return (key, ttl);
        }

        #endregion
        
        #region Grouped Observations (claves: "groupedObs:{field}:patient:{id}")
        public static string GroupedObs(ObjectId patientId, string groupedFieldName)
            => $"groupedObs:{groupedFieldName}:patient:{patientId}";

        public static (string Key, TimeSpan? Ttl) GroupedObsKeyWithTtl(
            CacheSettings? settings, ObjectId patientId, string groupedFieldName)
        {
            var key = GroupedObs(patientId, groupedFieldName);
            var ttl = CacheKeyTtl.ResolveForEntity(settings, CacheEnum.EntityType.GroupedObservations);
            return (key, ttl);
        }
        
        #endregion

        //Helpers
        public static string Normalize(IEnumerable<string> items)
            => string.Join("|", items
                .Where(s => !string.IsNullOrWhiteSpace(s))
                .Select(s => s.Trim())
                .OrderBy(s => s, StringComparer.Ordinal));
    }


    //  CLASSIFIER
    /// <summary>
    /// Clasificador de claves basado en prefijos.
    /// Permite al CacheDispatcher seleccionar el backend adecuado (Redis, InMemory, None).
    /// </summary>
    public static class CacheKeyClassifier
    {
        public static CacheEnum.EntityType Classify(string key)
        {
            if (key.StartsWith("patients:", StringComparison.OrdinalIgnoreCase))
                return CacheEnum.EntityType.Patients;

            if (key.StartsWith("displays:", StringComparison.OrdinalIgnoreCase) ||
                key.StartsWith("configDisplays:", StringComparison.OrdinalIgnoreCase))
                return CacheEnum.EntityType.Displays;

            if (key.StartsWith("pumpObs:", StringComparison.OrdinalIgnoreCase))
                return CacheEnum.EntityType.PumpObservations;

            if (key.StartsWith("appointments:", StringComparison.OrdinalIgnoreCase))
                return CacheEnum.EntityType.Appointments;

            if (key.StartsWith("groupedObs:", StringComparison.OrdinalIgnoreCase))
                return CacheEnum.EntityType.GroupedObservations;
            
            if (key.StartsWith("configObservations:", StringComparison.OrdinalIgnoreCase))
                return CacheEnum.EntityType.ConfigObservations;
            
            return CacheEnum.EntityType.Unknown;
        }
    }

    //  PATTERNS
    /// <summary>
    /// Patrones para DeleteByPattern (Redis) u operaciones masivas por prefijo.
    /// </summary>
    public static class CacheKeyPatterns
    {
        public static string ForEntity(CacheEnum.EntityType type)
            => type switch
            {
                CacheEnum.EntityType.Patients            => "patients:*",
                CacheEnum.EntityType.Displays            => "displays:*",
                CacheEnum.EntityType.PumpObservations    => "pumpObs:*",
                CacheEnum.EntityType.Appointments        => "appointments:*",
                CacheEnum.EntityType.GroupedObservations => "groupedObs:*",
                CacheEnum.EntityType.ConfigObservations  => "configObservation:*",
                _                                        => "*"
            };

        public static string ByPrefix(string prefix)
            => $"{prefix}:*";

        public static string ByPatient(string prefix, string patientId)
            => $"{prefix}:*:{patientId}*";

        public static string ByDate(string prefix, string date)
            => $"{prefix}:*:{date}";
    }

    //  INSPECTOR (diagnóstico)
    /// <summary>
    /// Utilidad para extraer información estructurada útil para trazas.
    /// No usada por la lógica de caché, pero sí por logs y diagnósticos.
    /// </summary>
    public static class CacheKeyInspector
    {
        private static readonly Regex _patientRegex =
            new(@"patients:(?<id>[^:]+)", RegexOptions.Compiled);

        private static readonly Regex _groupedRegex =
            new(@"groupedObs:(?<field>[^:]+):(?<id>[^:]+)", RegexOptions.Compiled);

        private static readonly Regex _appointmentDayRegex =
            new(@"appointments:(?<location>.+):(?<date>\d{8})", RegexOptions.Compiled);

        public static string? ExtractPatientId(string key)
        {
            var match = _patientRegex.Match(key);
            return match.Success ? match.Groups["id"].Value : null;
        }

        public static (string? Field, string? PatientId) ExtractGroupedObservationInfo(string key)
        {
            var match = _groupedRegex.Match(key);
            return !match.Success
                ? (null, null)
                : (match.Groups["field"].Value, match.Groups["id"].Value);
        }

        public static (string? Location, DateTime? Date) ExtractAppointmentInfo(string key)
        {
            var match = _appointmentDayRegex.Match(key);
            if (!match.Success)
                return (null, null);

            var location = match.Groups["location"].Value;
            var dateStr  = match.Groups["date"].Value;

            if (DateTime.TryParseExact(
                dateStr, "yyyyMMdd", null,
                System.Globalization.DateTimeStyles.None,
                out var date))
                return (location, date);

            return (location, null);
        }
    }
}