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;

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());
    }

    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;
    }

    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);
    }

    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));
            }
    }

    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));
            }
    }

    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;
        }
    }

    public Task<Relay?> GetById(ObjectId relay)
    {
        return _relayRepository.GetById(relay);
    }
    public List<Relay> GetRelayInList(List<ObjectId>? relayList)
    {
        if(relayList == null) return new List<Relay>();
        return _relayRepository.GetRelayInList(relayList);
    }

    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);
    }

    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);
    }

    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);
        }
    }

    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;
    }

    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);
        }
    }

    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();
    }


    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;
    }
}