using System.Collections.Concurrent;
using System.Net;
using System.Text;
using System.Web;
using adas_core.Application.Repositories.Interfaces;
using adas_core.Application.Services.Interfaces;
using adas_core.Domain.Enums;
using adas_core.Domain.Exceptions;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Responses;
using adas_core.Infrastructure.Utils;
using adas_core.module.Relays.Devices;
using adas_core.module.Relays.Models;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using MongoDB.Bson;
using MongoDB.Driver;
using Quartz.Util;

namespace adas_core.Infrastructure.Services;

/// <summary>
/// Provides a concrete implementation of the <see cref="IRelayService"/> contract,
/// delivering relay-related functionality to consumers of the service.
/// </summary>
/// <remarks>
/// This class is the default implementation of <see cref="IRelayService"/>,
/// and can be substituted via dependency injection where the interface is required.
/// </remarks>
public class RelayService : IRelayService
{
    private readonly List<RelayDevice> _devices = [];
    private readonly IHttpClientFactory _httpClientFactory;
    private readonly ILogger<RelayService> _logger;
    private readonly IPointOfCareService _pointOfCareService;
    private readonly RelaySettings _relaySettings;
    private readonly IRelayRepository _relayRepository;

    private readonly ConcurrentDictionary<Tuple<string, int, int>, RelayEnum.Status> _relayWithStatus = new();
    private readonly string _url;
    private UriBuilder? _builder;

    public RelayService(
        ILogger<RelayService> logger,
        IHttpClientFactory httpClientFactory,
        IOptions<RelaySettings> relaySettings,
        IPointOfCareService pointOfCareService,
        IRelayRepository relayRepository)
    {
        _httpClientFactory = httpClientFactory;
        _pointOfCareService = pointOfCareService;
        _relayRepository = relayRepository;
        _logger = logger;
        _relaySettings = relaySettings.Value;
        _url = _relaySettings.RecordingOrApiUrl ?? string.Empty;

        Task.Run(async () => await InitRelayWithStatus());
    }

    /// <summary>
    /// Asynchronously determines the current status of a relay, preferring a cached value when available and falling back to a direct device read or an external endpoint when the device is unreachable.
    /// Returns the cached status if caching is enabled, the cached value is present and not <see cref="RelayEnum.Status.NotInitialized"/>; otherwise queries the relay device, or invokes the configured URL endpoint if the device cannot be obtained.
    /// On failure, logs the error and resolves the status to <see cref="RelayEnum.Status.Unknown"/>.
    /// </summary>
    /// <param name="relay">The relay to check, identified by its IP, port, and relay number used both as the cache key and as the target of the status request.</param>
    /// <returns>A task that yields the resolved <see cref="RelayEnum.Status"/> of the relay, or <see cref="RelayEnum.Status.Unknown"/> when the status cannot be determined.</returns>
    public async Task<RelayEnum.Status> CheckRelayStatus(Relay relay)
        {
            var cacheKey = Tuple.Create(relay.Ip, relay.Port, relay.RelayNumber);
            try
            {
                if (_relaySettings.Cache && relay.Cache)
                    lock (_relayWithStatus)
                    {
                        if (_relayWithStatus.TryGetValue(cacheKey, out var status) &&
                            status != RelayEnum.Status.NotInitialized)
                            return status;
    
                        _logger.LogDebug("Relay status not found in cache: {Dct}",
                            DictionaryToString(_relayWithStatus));
                    }
    
                RelayDevice? relayDevice = null;
                try
                {
                    relayDevice = GetRelayDevice(relay);
                }
                catch (Exception e)
                {
                    _logger.LogWarning("Error getting relay device: {EMessage} {EStackTrace}", e.Message, e.StackTrace);
                }
    
                if (relayDevice == null && !string.IsNullOrEmpty(_url)) return await GetRelayStatusByOr(relay);
    
                if (relayDevice != null) return relayDevice.GetStatusRelay(relay.RelayNumber);
            }
            catch (Exception e)
            {
                _logger.LogError("Error CheckRelayStatus relay {Relay}: {EMessage} {EStackTrace}", relay, e.Message,
                    e.StackTrace);
    
                lock (_relayWithStatus)
                {
                    _relayWithStatus[cacheKey] = RelayEnum.Status.Unknown;
                }
            }
    
            return RelayEnum.Status.Unknown;
        }

    /// <summary>
    /// Checks the status of a relay identified by its unique ID. Returns <see cref="RelayEnum.Status.Unknown"/> when the relay cannot be found, otherwise delegates to the relay-based overload to resolve the current status.
    /// </summary>
    /// <param name="relayId">The unique identifier of the relay whose status should be checked.</param>
    /// <returns>The current <see cref="RelayEnum.Status"/> of the relay, or <see cref="RelayEnum.Status.Unknown"/> if no relay with the given ID exists.</returns>
    public async Task<RelayEnum.Status> CheckRelayStatus(ObjectId relayId)
        {
            var relay = await _relayRepository.GetById(relayId);
            if(relay == null) return RelayEnum.Status.Unknown;
            return await CheckRelayStatus(relay);
        }

    /// <summary>
    /// Asynchronously powers off the specified relay. Uses a cache to avoid redundant operations when the relay is already off, falls back to an HTTP request when no local relay device is available, and updates the cache after a successful power off.
    /// </summary>
    /// <param name="relay">The relay to power off, including its network address and relay number.</param>
    public async Task PowerOff(Relay relay)
        {
            var cacheKey = Tuple.Create(relay.Ip, relay.Port, relay.RelayNumber);
            if (_relaySettings.Cache && relay.Cache)
                lock (_relayWithStatus)
                {
                    if (_relayWithStatus.Any() &&
                        _relayWithStatus[cacheKey] == RelayEnum.Status.Off)
                        return;
                }
    
            RelayDevice? relayDevice = null;
            try
            {
                relayDevice = GetRelayDevice(relay);
            }
            catch (Exception e)
            {
                _logger.LogWarning("Error getting relay device: {EMessage} {EStackTrace}", e.Message, e.StackTrace);
            }
    
            if (relayDevice == null && !string.IsNullOrEmpty(_url))
            {
                _builder = new UriBuilder(_url)
                {
                    Path = $"Relay/{relay.RelayNumber}/powerOff"
                };
                _logger.LogDebug("Send PowerOff relay {Relay}", relay);
                await PowerRelay(relay, _builder);
            }
    
            relayDevice?.PowerOffRelay(relay.RelayNumber);
    
            if (_relaySettings.Cache && relay.Cache)
                lock (_relayWithStatus)
                {
                    _relayWithStatus[cacheKey] = RelayEnum.Status.Off;
                    _logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus));
                }
        }

    /// <summary>
    /// Powers on the specified relay. If the relay is configured with a URL, sends a remote power-on request; otherwise, drives the relay device directly. Honors a caching layer to avoid redundant power-on commands when the relay is already reported as on.
    /// </summary>
    /// <param name="relay">The relay to power on, including driver, IP, port, relay number, and cache settings.</param>
    public async Task PowerOn(Relay relay)
        {
            if (string.IsNullOrEmpty(relay.Driver) || string.IsNullOrEmpty(relay.Ip)) return;
            var cacheKey = Tuple.Create(relay.Ip, relay.Port, relay.RelayNumber);
            if (_relaySettings.Cache && relay.Cache)
                if (_relaySettings.Cache && relay.Cache)
                    lock (_relayWithStatus)
                    {
                        if (_relayWithStatus.Any() &&
                            _relayWithStatus[cacheKey] == RelayEnum.Status.On)
                            return;
                    }
    
    
            RelayDevice? relayDevice = null;
            try
            {
                relayDevice = GetRelayDevice(relay);
            }
            catch (Exception e)
            {
                _logger.LogWarning("Error getting relay device: {EMessage} {EStackTrace}", e.Message, e.StackTrace);
            }
    
            if (relayDevice == null && !string.IsNullOrEmpty(_url))
            {
                _builder = new UriBuilder(_url)
                {
                    Path = $"Relay/{relay.RelayNumber}/powerOn"
                };
    
                _logger.LogDebug("Send PowerOn relay {Relay}", relay);
    
                await PowerRelay(relay, _builder);
            }
    
            relayDevice?.PowerOnRelay(relay.RelayNumber);
    
    
            if (_relaySettings.Cache && relay.Cache)
                lock (_relayWithStatus)
                {
                    _relayWithStatus[cacheKey] = RelayEnum.Status.On;
                    _logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus));
                }
        }

    /// <summary>
    /// Sets the manual relay status for a point of care device. If the point of care exists and has a configured relay list, the relay matching the specified type is updated with the new status; otherwise, the operation is silently skipped.
    /// </summary>
    /// <param name="status">The manual relay status to apply.</param>
    /// <param name="pocId">The identifier of the point of care device whose relay configuration should be updated.</param>
    /// <param name="type">The relay type used to look up the target relay within the point of care's relay configuration.</param>
    public async Task SetManualRelay(RelayEnum.Status status, ObjectId pocId, RelayEnum.Type type)
        {
            try
            {
                var poc = await _pointOfCareService.FindById(pocId);
                if (poc is { Configuration.RelayIdList: not null })
                {
                    var relay = _relayRepository.GetRelayByTypeInList(poc.Configuration.RelayIdList, type).FirstOrDefault();
                    if (relay != null) relay.ManualRelayStatus = status;
    
                    await _pointOfCareService.UpdateRelayConfig(poc);
                    _logger.LogDebug("SetManualRelay: {Status}, pocId: {Location}, bed: {Bed}", status, poc.Id, poc.Bed);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError("Error settings manual relay. Exception: {ex}", ex);
                throw;
            }
        }

    /// <summary>
    /// Retrieves a relay entity by its unique identifier from the repository.
    /// Returns <c>null</c> when no matching relay is found.
    /// </summary>
    /// <param name="relay">The unique identifier of the relay to look up.</param>
    /// <returns>A task that resolves to the <see cref="Relay"/> if found, or <c>null</c> if no relay matches the supplied identifier.</returns>
    public Task<Relay?> GetById(ObjectId relay)
        {
            return _relayRepository.GetById(relay);
        }
    /// <summary>
    /// Retrieves a list of relays matching the specified list of relay identifiers. Returns an empty list when the provided identifier list is null.
    /// </summary>
    /// <param name="relayList">The list of relay identifiers to look up. Can be null.</param>
    /// <returns>A list of <see cref="Relay"/> objects corresponding to the provided identifiers, or an empty list if the input is null.</returns>
    public List<Relay> GetRelayInList(List<ObjectId>? relayList)
        {
            if(relayList == null) return new List<Relay>();
            return _relayRepository.GetRelayInList(relayList);
        }

    /// <summary>
    /// Retrieves the relays of a specified type from the provided list of configuration relay identifiers. Returns an empty list when the input list is <c>null</c>, otherwise delegates the lookup to the relay repository.
    /// </summary>
    /// <param name="configurationRelayList">The optional list of <see cref="ObjectId"/> values identifying the configuration relays to filter; when <c>null</c>, the method short-circuits and returns an empty list.</param>
    /// <param name="type">The relay type used to filter the matching relays within the supplied list.</param>
    /// <returns>A <see cref="List{Relay}"/> containing the relays matching the specified <paramref name="type"/>, or an empty list if <paramref name="configurationRelayList"/> is <c>null</c>.</returns>
    public List<Relay> GetRelayByTypeInList(List<ObjectId>? configurationRelayList, RelayEnum.Type type)
        {
            if(configurationRelayList == null) return new List<Relay>();
            return _relayRepository.GetRelayByTypeInList(configurationRelayList, type);
        }

    public async Task<PaginationResponse<Relay>> GetPaginatedRelays(PaginationFilter filter)
    {
        var usedRelayIds = await _pointOfCareService.FindAllIdRelaysInUse();

        var fluentQuery = _relayRepository.GetPaginatedRelays(filter);

        if (filter.FilteredRequest?.InUse != null)
        {
            bool filterInUse = filter.FilteredRequest.InUse.Value;
            var filterBuilder = Builders<Relay>.Filter;
        
            var idFilter = filterInUse 
                ? filterBuilder.In(c => c.Id, usedRelayIds)
                : filterBuilder.Not(filterBuilder.In(c => c.Id, usedRelayIds));

            fluentQuery.Filter = filterBuilder.And(fluentQuery.Filter, idFilter);
        }

        var count = await fluentQuery.CountDocumentsAsync();
        var data = await fluentQuery
            .Skip((filter.PageNumber - 1) * filter.PageSize)
            .Limit(filter.PageSize)
            .ToListAsync();

        if (data == null) return new PaginationResponse<Relay>([], filter.PageNumber, filter.PageSize, count);

        foreach (var camera in data)
        {
            if (camera == null) continue;

            bool isInUse = usedRelayIds.Contains(camera.Id);
        
            // Asignación mediante reflexión para el private set
            camera.GetType().GetProperty(nameof(Relay.InUse))
                ?.SetValue(camera, isInUse);
        }

        return new PaginationResponse<Relay>(data, filter.PageNumber, filter.PageSize, count);
    }

    /// <summary>
    /// Inserts a new relay into the repository after verifying that no relay with the same name already exists.
    /// </summary>
    /// <param name="request">The relay entity to insert, whose name is checked for duplicates prior to persistence.</param>
    /// <returns>The inserted <see cref="Relay"/> entity returned by the repository, or <c>null</c> if the repository yields no result.</returns>
    /// <exception cref="Exception">Thrown when a relay with the same name as <paramref name="request"/> already exists in the repository.</exception>
    public async Task<Relay?> InsertRelay(Relay request)
        {
            var relayExist = await _relayRepository.GetByName(request.RelayName);
            if(relayExist != null) throw new Exception($"Relay with name {request.RelayName} already exists");
            return await _relayRepository.InsertOneRelayAsync(request);
        }

    /// <summary>
    /// Updates an existing relay identified by its unique ObjectId by delegating to the relay repository.
    /// </summary>
    /// <param name="objectId">The unique identifier of the relay to update.</param>
    /// <param name="relay">The relay object containing the updated information.</param>
    /// <returns>A task representing the asynchronous operation, containing the updated <see cref="Relay"/>, or <c>null</c> if no relay with the specified identifier was found.</returns>
    public Task<Relay?> UpdateRelayById(ObjectId objectId, Relay relay)
        {
            return _relayRepository.UpdateRelayAsync(objectId, relay);
        }

    public event EventHandler<Tuple<RelayDevice, int>>? RelayStatusChanged;

    private async Task InitRelayWithStatus()
    {
        try
        {
            var pocConfigList = await _pointOfCareService.GetAllConfigs();
            var relayTasks = new List<Task>();

            pocConfigList.ForEach(poc =>
            {
                if (poc.Configuration?.RelayList == null) return;
                relayTasks.AddRange(from relayConf in poc.Configuration.RelayList
                    let statusTask = CheckRelayStatus(relayConf)
                    select statusTask.ContinueWith(task =>
                    {
                        if (task.IsCompletedSuccessfully)
                        {
                            var cackeKey = Tuple.Create(relayConf.Ip, relayConf.Port, relayConf.RelayNumber);
                            if (!relayConf.Cache || !relayConf.Cache) return;
                            lock (_relayWithStatus)
                            {
                                _relayWithStatus[cackeKey] = task.Result;
                            }
                        }
                        else if (task.IsFaulted)
                        {
                            // Manejar la excepción si la llamada asincrónica falla
                            _logger.LogError("Error al obtener el estado del relé {RelayParsedIp}: {TaskException}",
                                relayConf.Ip, task.Exception.Message);
                        }
                    }));
            });

            await Task.WhenAll(relayTasks);

            lock (_relayWithStatus)
            {
                _logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus));
            }
        }
        catch (Exception ex)
        {
            Console.WriteLine("Exception setting relay with status: " + ex.Message);
        }
    }

    /// <summary>
    /// Retrieves a <see cref="RelayDevice"/> for the given relay, returning a cached instance when available
    /// or dynamically creating and registering a new one based on the relay's driver type. Returns <c>null</c>
    /// when the relay has no IP address or its port is zero.
    /// </summary>
    /// <param name="relay">The relay configuration used to locate an existing device or instantiate a new one.</param>
    /// <returns>The existing or newly created <see cref="RelayDevice"/>, or <c>null</c> if the relay is missing a valid IP or port.</returns>
    /// <exception cref="AdasException">Thrown when no constructor matching <see cref="Relay"/> and <see cref="RelaySettings"/> is found on the resolved driver type.</exception>
    /// <exception cref="AdasException">Thrown when the resolved driver type cannot be instantiated into a <see cref="RelayDevice"/>.</exception>
    private RelayDevice? GetRelayDevice(Relay relay)
        {
            if(relay.Ip.IsNullOrWhiteSpace() || relay.Port == 0) return null;
            var device = _devices.FirstOrDefault(d => d.Relay.Ip == relay.Ip && d.Relay.Port == relay.Port);
            if (device != null) return device;
    
            var type = TypesUtils.GetDriver("Relay", relay.Driver ?? string.Empty);
            var constructor = type.GetConstructor([typeof(Relay), typeof(RelaySettings)]);
            if (constructor == null) throw new AdasException("Constructor not found for relay device");
            var relayDevice = (RelayDevice)constructor.Invoke([relay, _relaySettings]);
            if (relayDevice == null) throw new AdasException("Relay device not found");
            relayDevice.RelayStatusChanged += (_, outletId) =>
            {
                var cacheKey = Tuple.Create(relay.Ip, relay.Port, outletId);
                var value = relayDevice.GetStatusRelay(outletId);
                lock (_relayWithStatus)
                {
                    _relayWithStatus[cacheKey] = value;
                }
    
                _logger.LogDebug("Relay status changed for {Key} with value {Value}", cacheKey, value);
                RelayStatusChanged?.Invoke(this, Tuple.Create(relayDevice, outletId));
            };
            _devices.Add(relayDevice);
            return relayDevice;
        }

    /// <summary>
    /// Sends a power relay request by appending relay configuration details (driver, host, port, name, total count, and credentials) as query parameters to the provided URI and issuing an HTTP POST. Logs a debug message when the response status is not OK, and logs and swallows any exception encountered during the request.
    /// </summary>
    /// <param name="relay">The relay whose driver, IP, port, name, total count, username, and password are included in the request query string.</param>
    /// <param name="builder">The URI builder whose query is updated with the relay parameters and whose resulting URI is used as the request target.</param>
    private async Task PowerRelay(Relay relay, UriBuilder builder)
        {
            var query = HttpUtility.ParseQueryString(builder.Query);
            query["driver"] = relay.Driver;
            query["host"] = relay.Ip;
            query["port"] = relay.Port.ToString();
            query["relayName"] = relay.RelayName;
            query["relays"] = relay.Total.ToString();
            query["username"] = relay.Username;
            query["password"] = relay.Password;
    
            builder.Query = query.ToString();
            HttpRequestMessage request = new()
            {
                RequestUri = builder.Uri,
                Method = HttpMethod.Post
            };
    
            try
            {
                using var client = _httpClientFactory.CreateClient();
                var httpResponse = await client.SendAsync(request);
    
                if (!httpResponse.StatusCode.Equals(HttpStatusCode.OK))
                    _logger.LogDebug("Send PowerRelay relay {Relay}", relay);
            }
            catch (Exception e)
            {
                _logger.LogError("Error PowerRelay relay {Relay}: {EMessage} {EStackTrace}", relay, e.Message,
                    e.StackTrace);
            }
        }

    /// <summary>
    /// Converts a concurrent dictionary of relay statuses into a human-readable string representation, formatting each entry as a line containing the device ID, port, and current status.
    /// </summary>
    /// <param name="dictionary">The concurrent dictionary containing relay status entries keyed by a tuple of device ID, port, and an additional integer value.</param>
    /// <returns>A string containing one formatted line per dictionary entry describing the device ID, port, and status.</returns>
    private static string DictionaryToString(ConcurrentDictionary<Tuple<string, int, int>, RelayEnum.Status> dictionary)
        {
            var builder = new StringBuilder();
    
            foreach (var pair in dictionary)
                builder.AppendLine($"Device ID: {pair.Key.Item1}, Port: {pair.Key.Item2}, Status: {pair.Value}");
    
            return builder.ToString();
        }


    /// <summary>
    /// Retrieves the current status of the specified relay by querying its external service endpoint, and caches the result (success or failure) when caching is enabled for both the application and the relay.
    /// Falls back to <see cref="RelayEnum.Status.Unknown"/> when the HTTP response is not successful.
    /// </summary>
    /// <param name="relay">The relay instance whose status should be queried; its IP, port, relay number, driver, credentials, and mode are used to build the request.</param>
    /// <returns>The parsed <see cref="RelayEnum.Status"/> returned by the remote relay service, or <see cref="RelayEnum.Status.Unknown"/> if the request fails.</returns>
    private async Task<RelayEnum.Status> GetRelayStatusByOr(Relay relay)
        {
            var cacheKey = Tuple.Create(relay.Ip, relay.Port, relay.RelayNumber);
            _builder = new UriBuilder(_url)
            {
                Path = $"Relay/{relay.RelayNumber}/status"
            };
    
            var query = HttpUtility.ParseQueryString(_builder.Query);
            query["driver"] = relay.Driver;
            query["host"] = relay.Ip;
            query["port"] = relay.Port.ToString();
            query["relayName"] = relay.RelayName;
            query["relays"] = relay.Total.ToString();
            query["username"] = relay.Username;
            query["password"] = relay.Password;
            query["mode"] = relay.Mode.ToString();
            query["refreshTime"] = "0";
    
            _builder.Query = query.ToString();
            HttpRequestMessage request = new()
            {
                RequestUri = _builder.Uri,
                Method = HttpMethod.Get
            };
    
    
            using var client = _httpClientFactory.CreateClient();
            client.Timeout = new TimeSpan(0, 0, 3);
            var httpResponse = await client.SendAsync(request);
    
            if (httpResponse.StatusCode.Equals(HttpStatusCode.OK))
            {
                var responseContent = httpResponse.Content;
                var response = await responseContent.ReadAsStringAsync();
    
                response = response.Replace("\"", "");
    
                var relayStatusParsed = (RelayEnum.Status)Enum.Parse(typeof(RelayEnum.Status), response);
    
                if (_relaySettings.Cache && relay.Cache)
                    lock (_relayWithStatus)
                    {
                        _relayWithStatus[cacheKey] = relayStatusParsed;
                        _logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus));
                    }
    
    
                return relayStatusParsed;
            }
    
            if (_relaySettings.Cache && relay.Cache)
                lock (_relayWithStatus)
                {
                    _relayWithStatus[cacheKey] = RelayEnum.Status.Unknown;
                    _logger.LogDebug("Relay status cached: {Dct}", DictionaryToString(_relayWithStatus));
                }
    
            return RelayEnum.Status.Unknown;
        }
}