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.AppSettings;
using adas_core.Domain.Models.DTO;
using adas_core.Domain.Models.DTO.Display;
using adas_core.Domain.Models.Filter;
using adas_core.Domain.Models.Masters;
using adas_core.Domain.Models.MongoModels;
using adas_core.Domain.Models.Responses;
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;

public class DisplayService(
    IDisplayRepository displayRepository,
    IPointOfCareService pointOfCareService,
    Lazy<IUnitService> unitService,
    IDisplayConfigService displayConfigService,
    ISubscribersService subscribersService,
    IClientMessageService clientMessageService,
    IUserRepository userRepository,
    IAuthService authorityService,
    ILogger<DisplayService> logger,
    IHttpContextAccessor httpContextAccessor,
    ILocalAuditService auditService,
    Lazy<IPermissionService> permissionService,
    ICacheService cacheService,
    IOptions<CacheSettings> cacheSettings)
    : IDisplayService
{
    private readonly CacheSettings?  _cacheSettings =  cacheSettings.Value ;
    
    #region Methods

    #region Create

    public async Task<Display> InsertOne(Display display)
    {
        var defaultConfig = await displayConfigService.GetDefaultConfig(display.Type) ??
                            throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
        display.DisplayConfigId = defaultConfig.Id;
        await displayRepository.InsertOneAsync(display);
        await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, display);
        return display;
    }

    public async Task<Display> InsertOneTest()
    {
        var d = new Display
        {
            Name = "DisplayTEST",
            UnitId = new ObjectId("65ba5f89d5ba8e273cf9cd96"),
            DisplayConfigId = new ObjectId("45ba5f89d5ba8e273cf9cd96")
        };
        await displayRepository.InsertOneAsync(d);
        await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, d);
        return d;
    }

    #endregion

    #region Read

    public async Task<List<DisplayMinimalDto>> GetAllCompact()
    {
        var result = await displayRepository.GetAll();
        List<DisplayMinimalDto> listToReturn = [];
        foreach (var res in result) listToReturn.Add(new DisplayMinimalDto(res));
        return listToReturn;
    }

    public Task<List<Display>> GetAll(string? userName)
    {
        throw new NotImplementedException();
    }

    public async Task<PaginationResponse<Display>> GetPaginatedDisplays(PaginationFilter filter)
    {
        var result = displayRepository.GetPaginatedDisplays(filter);

        var count = await result.CountDocumentsAsync();

        var data = await result.Skip((filter.PageNumber - 1) * filter.PageSize)
            .Limit(filter.PageSize)
            .ToCursorAsync();

        var dataList = await data.ToListAsync();

        return new PaginationResponse<Display>(dataList, filter.PageNumber, filter.PageSize, count);
    }

    public async Task<List<DisplayWithPermissionsDto>> GetAllByUser(string? userName)
    {
        var start = DateTime.Now;
        var listToReturn = new List<DisplayWithPermissionsDto>();
        if (userName == null) return listToReturn;
        var user = await userRepository.GetByUserAndAuthoritesName(userName);
        if (user == null) return listToReturn;
        if (user.Authorization == null || user.Authorization.Count == 0)
            user.Authorization = await authorityService.GetUserAuthorities(user.Id);

        if (user.Authorization == null)
            return listToReturn;

        foreach (var e in user.Authorization)
            if (e.UnitId != null)
            {
                var isParsed = ObjectId.TryParse(e.UnitId, out var dId);
                if (isParsed)
                {
                    var dis = await displayRepository.GetByUnitId(dId);
                    foreach (var display in dis)
                    {
                        var toAdd = await GetInfo(display.Id, userName, user.Authorization,null, false, false, false, false);

                        if (toAdd != null)
                        {
                            var newDto = new DisplayWithPermissionsDto
                            {
                                Display = toAdd,
                                Permissions =
                                    await permissionService.Value.GetPermissionsForUnit(dId.ToString(), user) ??
                                    throw new ForbbidenException(HttpEnum.ErrorMessage.ForbiddenNoPermission)
                            };
                            listToReturn.Add(newDto);
                        }
                    }
                }
            }
            else if (e.DisplayId != null && !FindDisplayInPerms(e.DisplayId, listToReturn))
            {
                var isParsed = ObjectId.TryParse(e.DisplayId, out var dId);
                if (isParsed)
                {
                    var toAdd = await GetInfo(dId, userName, user.Authorization, null, false, false, false, false);

                    if (toAdd != null)
                    {
                        var newDto = new DisplayWithPermissionsDto
                        {
                            Display = toAdd,
                            Permissions = await permissionService.Value.GetPermissionsForDisplay(toAdd, user)
                        };
                        listToReturn.Add(newDto);
                    }
                }
            }

        var end = DateTime.Now;
        logger.LogDebug("Finished GetAllByUser Displays for user {user} in {TotalSeconds:F1} seconds", userName,
            (end - start).TotalSeconds);
        return listToReturn;
    }

    private static bool FindDisplayInPerms(string displayId, List<DisplayWithPermissionsDto> perms)
    {
        return perms.Any(p => p.Display != null && p.Display.Id.ToString() == displayId);
    }

    public async Task<List<Display>> GetByType(DisplayConfigEnums.DisplayType type)
    {
        var configs = await displayConfigService.GetByType(type);
        var listToReturn = new List<Display>();
        foreach (var config in configs)
        {
            var displayToAdd = await displayRepository.GetByConfigId(config.Id);
            displayToAdd.ForEach(c => c.DisplayConfig = config);
            if (!displayToAdd.IsNullOrEmpty()) listToReturn.AddRange(displayToAdd);
        }

        return listToReturn;
    }

    public async Task<List<Display>> GetByPointOfCare(PointOfCare pointOfCare)
    {
        return await displayRepository.GetByPointOfCare(pointOfCare);
    }

    public Task<List<Display>> GetByConfigId(ObjectId configId)
    {
        return displayRepository.GetByConfigId(configId);
    }

    public Task<List<Display>> GetByCardConfigId(ObjectId configId)
    {
        return displayRepository.GetByCardConfigId(configId);
    }

    public async Task<Display?> GetByName(string name)
    {
        return await displayRepository.GetByName(name) ??
               throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
    }

    public async Task<Display?> GetById(ObjectId id)
    {
        return await displayRepository.GetById(id);
    }

    public async Task<DisplayWithPermissionsDto> GetByIdWithPermissions(ObjectId id, LocaleEnum localeEnum)
    {
        var username = JwtHelper.GetUsernameFromPrincipal(httpContextAccessor.HttpContext?.User!) ??
                       throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
        var user = await userRepository.GetByUserName(username);
        var display = await GetById(id);

        if (display == null) throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);

        foreach (var poc in display.PointOfCareIdList)
        {
            var c = await pointOfCareService.GetInfo(poc, localeEnum);
            if (c != null) display.PointOfCares.Add(c);
        }

        display.DisplayConfig =
            await displayConfigService.GetById(display.DisplayConfigId, display.UnitId, display.Type);
        display.DisplayConfig!.DisplaySectionList = [];
        try
        {
            display.DisplayConfig!.DisplaySectionList =
                await GetDisplaySectionByUser(display.Type, id, username, user?.Authorization);
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }

        return new DisplayWithPermissionsDto
        {
            Display = display,
            Permissions = await permissionService.Value.GetPermissionsForDisplay(display, user!)
        };
    }

    public async Task<long> CountDisplaysByUnitId(ObjectId unitId)
    {
        return await displayRepository.CountByUnitId(unitId);
    }

    public async Task<Display?> GetInfo(ObjectId id, 
        string? userName, 
        List<Authorization>? authorizations,
        LocaleEnum? locale, 
        bool fillPointOfCare = true,
        bool fillPatientData = false, 
        bool fillDisplayList = true, 
        bool fillDisplayConfig = true,
        CancellationToken ct = default)
    {
        var start = DateTime.Now;

        Display? display;
        if (fillDisplayConfig)
        {
            
            // Clave: display con configuración
            var (key, ttl) = CacheKeys.DisplayWithConfigKeyWithTtl(_cacheSettings, id);

            display = await cacheService.GetOrSetObjectAsync(
                key,
                async () => await BuildDisplayWithConfig(id, ct),
                ttl,
                ct);
            
        }
        else
        {
            // Clave: display base
            var (key, ttl) = CacheKeys.DisplayBaseKeyWithTtl(_cacheSettings, id);

            display = await cacheService.GetOrSetObjectAsync(
                key,
                async () => await displayRepository.GetById(id),
                ttl,
                ct);

        }
        if (display == null) return null;
        
        // PointOfCare (cacheado en su propio servicio)
        if (fillPointOfCare)
            foreach (var poc in display.PointOfCareIdList)
            {
                var c = await pointOfCareService.GetInfo(poc, locale, fillPatientData);
                if (c != null) display.PointOfCares.Add(c);
            }

        if (authorizations == null && userName != null)
        {
            var c = await userRepository.GetByUserAndAuthoritesName(userName);
            authorizations = c?.Authorization;
        }

        // DisplayList (depende de autorizaciones → NO cacheable)
        if (fillDisplayList)
            if (display.DisplayConfig != null)
                display.DisplayConfig.DisplaySectionList =
                    await GetDisplaySectionByUser(display.Type, id, userName, authorizations);
            else
                logger.LogError("DISPLAY CONFIG NULL on fill display list");

        var end = DateTime.Now;

        logger.LogDebug("Finished GetInfo Displays for user {user} in {TotalSeconds:F1} seconds", userName,
            (end - start).TotalSeconds);

        return display;
    }
   
    private async Task<Display?> BuildDisplayWithConfig(ObjectId id, CancellationToken ct)
    {
        var display = await displayRepository.GetById(id);
        if (display == null) return null;

        var type = display.Type;

        // DisplayConfig
        display.DisplayConfig =
            await displayConfigService.GetById(display.DisplayConfigId, display.UnitId, type);

        if (type != DisplayConfigEnums.DisplayType.SmartDisplay ||
            display.DisplayConfig is not SmartDisplay { CardRotatingLayout: not null } smart) 
            return display;
        
        if(smart.CardRotatingLayout== null)
            return display;

        foreach (var card in smart.CardRotatingLayout)
            card.Data = await displayConfigService.GetCardConfigById(card.DataId)
                        ?? new CardConfig();

        return display;
        
    }
    
    
    public async Task<List<MinimalDisplaySection>> GetDisplaySectionByUser(
        DisplayConfigEnums.DisplayType type,
        ObjectId? currentDisplay,
        string? userName,
        List<Authorization>? authorizations)
    {
        try
        {
            var listToReturn = new List<MinimalDisplaySection>();
            if (userName != null)
            {
                var user = await userRepository.GetByUserName(userName);

                if (user != null)
                {
                    var displayIdByAuthorities = authorizations ?? await authorityService.GetUserAuthorities(user.Id);

                    foreach (var e in displayIdByAuthorities)
                    {
                        var isParsed = ObjectId.TryParse(e.DisplayId, out var dId);
                        if (isParsed)
                        {
                            if (listToReturn.All(c => c.Id != dId))
                            {
                                var toAdd = await GetById(dId);
                                if (toAdd != null)
                                    if (type == toAdd.Type)
                                    {
                                        var minDisSec = new MinimalDisplaySection
                                        {
                                            Name = toAdd.Name,
                                            Id = toAdd.Id,
                                            IsSelected = currentDisplay != null && toAdd.Id == currentDisplay
                                        };
                                        listToReturn.Add(minDisSec);
                                    }
                            }
                        }
                        else
                        {
                            var isParsedUnitId = ObjectId.TryParse(e.UnitId, out var uId);
                            if (isParsedUnitId)
                            {
                                var unitDisplays = await GetByUnitId(uId);
                                foreach (var disp in unitDisplays)
                                    if (type == disp.Type && listToReturn.All(c => c.Id != dId))
                                    {
                                        var minDisSec = new MinimalDisplaySection
                                        {
                                            Name = disp.Name,
                                            Id = disp.Id,
                                            IsSelected = currentDisplay != null && disp.Id == currentDisplay
                                        };
                                        listToReturn.Add(minDisSec);
                                    }
                            }
                        }
                    }
                }
            }

            return listToReturn;
        }
        catch (Exception e)
        {
            Console.WriteLine(e);
            throw;
        }
    }

    public async Task<MinimalDisplayListDto> GetAllDisplaySection()
    {
        var minimalDisplayListDto = new MinimalDisplayListDto();
        var listDisplayNurse = await GetByType(DisplayConfigEnums.DisplayType.DisplayNurse);
        foreach (var displayForAdmin in listDisplayNurse)
        {
            var minDisSec = new MinimalDisplaySection
            {
                Name = displayForAdmin.Name,
                Id = displayForAdmin.Id
            };
            minimalDisplayListDto.DisplayNurse.Add(minDisSec);
        }

        var listDisplaySmart = await GetByType(DisplayConfigEnums.DisplayType.SmartDisplay);
        foreach (var displayForAdmin in listDisplaySmart)
        {
            var minDisSec = new MinimalDisplaySection
            {
                Name = displayForAdmin.Name,
                Id = displayForAdmin.Id
            };
            minimalDisplayListDto.SmartDisplay.Add(minDisSec);
        }

        return minimalDisplayListDto;
    }

    public async Task<List<Display>> GetByUnitId(ObjectId unitId)
    {
        return await displayRepository.GetByUnitId(unitId);
    }

    public async Task<PocAndUnitDto> GetAllAvailablePoc(List<string> displayIds, bool excludeVirtual = false)
    {
        try
        {
            var listToReturn = new PocAndUnitDto();
            var listObjectId = new List<ObjectId>();
            foreach (var displayId in displayIds)
            {
                var isParsed = ObjectId.TryParse(displayId, out var dId);
                if (isParsed)
                {
                    var diplay = await displayRepository.GetById(dId);
                    if (diplay != null)
                        listObjectId.Add(diplay.UnitId);
                }
            }

            var distinctObjectIds = listObjectId.Distinct().ToList();
            foreach (var distinctObjectId in distinctObjectIds)
            {
                var unit = await unitService.Value.FindById(distinctObjectId) ??
                           throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
                var pocList = await pointOfCareService.FindByUnitAndStatus(distinctObjectId,
                    StatusEnum.PointOfCare.Available, excludeVirtual);
                foreach (var pointOfCare in pocList)
                {
                    var pocAv = new MinimalPocAndUnitDto
                    {
                        PocId = pointOfCare.Id,
                        PocName = pointOfCare.Bed,
                        UnitId = pointOfCare.UnitId,
                        UnitName = unit.Name
                    };
                    listToReturn.PocList.Add(pocAv);
                }
            }

            return listToReturn;
        }
        catch (Exception e)
        {
            logger.LogError("Error GetAllAvailablePoc {Error}", e.Message);
            return new PocAndUnitDto();
        }
    }

    public async Task<List<PointOfCare>> GetAllPocsByDisplayId(ObjectId id)
    {
        try
        {
            var pocList = new List<PointOfCare>();
            var display = await displayRepository.GetById(id);
            if (display == null)
                return pocList;

            foreach (var pocId in display.PointOfCareIdList)
            {
                var poc = await pointOfCareService.FindById(pocId);
                if (poc != null) pocList.Add(poc);
            }

            return pocList;
        }
        catch (Exception e)
        {
            logger.LogError("Error GetAllAvailablePoc {Error}", e.Message);
            return [];
        }
    }

    public async Task<List<DisplayConfigLocationDto>> GetDisplayConfigLocations(ObjectId displayConfigId)
    {
        var displays = await GetByConfigId(displayConfigId);
        var locations = new List<DisplayConfigLocationDto>();
        foreach (var display in displays)
        {
            var unit = await unitService.Value.FindById(display.UnitId);
            DisplayConfigLocationDto newLocation = new()
            {
                DisplayName = display.Name,
                UnitName = unit?.Name,
            };
            locations.Add(newLocation);
        }

        return locations;
    }

    public async Task<bool> IsDisplayConfigInUse(ObjectId displayConfigId)
    {
        return await displayRepository.IsDisplayConfigInUse(displayConfigId) > 0;
    }

    #endregion

    #region Update

    /*
     * En esta actualización se espera una resubscipción al id del display ya que actualizar los PoC conlleva actualizar
     * subscrioptor y locations para las observaciones
     */
    public async Task<Display?> UpdatePointOfCareList(ObjectId objectId, List<ObjectId> listPocObId)
    {
        var oldDisplay = await displayRepository.GetById(objectId) ??
                         throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
        var displayToReturn = await displayRepository.UpdatePointOfCareList(objectId, listPocObId) ??
                              throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
        
        await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(objectId));
        
        SendDisplayBroadcast(displayToReturn, OperationType.UpdateDisplayPoC);
        
        await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplay, displayToReturn);
        return displayToReturn;
    }

    public async Task<Display?> UpdateConfig(Display oldDisplay, DisplayConfig? newDisplayConfig)
    {
        var newDisplayConfigCast = new DisplayConfig();
        switch (newDisplayConfig?.Type)
        {
            case DisplayConfigEnums.DisplayType.DisplayNurse:
                newDisplayConfigCast = newDisplayConfig as DisplayNurse;
                break;
            case DisplayConfigEnums.DisplayType.SmartDisplay:
                newDisplayConfigCast = newDisplayConfig as SmartDisplay;
                break;
        }

        if (newDisplayConfigCast != null)
        {
            var displayToReturn = await displayRepository.UpdateConfig(oldDisplay.Id, newDisplayConfigCast);
            if (displayToReturn != null)
            {
                SendDisplayBroadcast(displayToReturn, OperationType.UpdateDisplayConfig);
                await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplay,
                    displayToReturn);
            }
            
            await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(oldDisplay.Id));
            
            return displayToReturn;
        }

        return null;
    }

    public async Task<Display?> UpdateConfigId(Display oldDisplay, ObjectId configId)
    {
        var displayToReturn = await displayRepository.UpdateConfigId(oldDisplay.Id, configId);
        if (displayToReturn != null)
        {
            await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(oldDisplay.Id));
            SendDisplayBroadcast(displayToReturn, OperationType.UpdateDisplayConfig);
            await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldDisplay, displayToReturn);
        }

        return displayToReturn;
    }

    public async Task<Display?> UpdateConfigPreset(ObjectId objectIdDisplay, ObjectId objectIdConfigDisplay)
    {
        var oldConfig = await displayConfigService.GetById(objectIdConfigDisplay);
        var result = await displayRepository.UpdateConfigPreset(objectIdDisplay, objectIdConfigDisplay);
        
        await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(objectIdDisplay));
        
        var config = await displayConfigService.GetById(objectIdConfigDisplay);
        await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, oldConfig, config);
        if (result == null || config == null)
            throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);

        var subscribers = subscribersService.GetSubscribers().Where(s => s.DisplayId == objectIdDisplay).ToList();
        switch (config.Type)
        {
            case DisplayConfigEnums.DisplayType.DisplayNurse:
                SendNurseDisplayBroadcast(subscribers, config as DisplayNurse);
                break;
            case DisplayConfigEnums.DisplayType.SmartDisplay:
                SendSmartDisplayBroadcast(subscribers, config as SmartDisplay);
                break;
            case DisplayConfigEnums.DisplayType.Unknown:
                break;
            default:
                throw new InvalidFormatException(HttpEnum.ErrorMessage.BadRequestIncorrectType);
        }

        return result;
    }

    public async Task<Display?> UpdateName(ObjectId id, string name)
    {
        var display = await displayRepository.GetById(id) ??
                      throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
        var newDisplay = await displayRepository.UpdateName(display, name);
        
        await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(id));
        
        await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, display, newDisplay);
        return newDisplay;
    }

    #endregion

    #region Delete

    public async Task<bool> DeleteDisplay(ObjectId id)
    {
        var display = await displayRepository.GetById(id) ??
                      throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
        await displayRepository.DeleteAsync(id);
        
        // Invalidar CACHE (colección completa)
        await cacheService.DeleteByPatternAsync(CacheKeys.DisplayBase(id));

        await authorityService.DeleteByDisplayId(id);
        
        await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, display, null);
        return true;
    }

    public async Task DeleteDisplaysByUnitId(ObjectId unitId)
    {
        await displayRepository.DeleteManyByUnitId(unitId);
        // Invalidar CACHE (colección completa)
        await cacheService.DeleteByPatternAsync(CacheKeyPatterns.ForEntity(CacheEnum.EntityType.Displays));
        
        await authorityService.DeleteByUnitId(unitId);
    }

    #endregion

    #region Send Notification

    private void SendSmartDisplayBroadcast(List<WsSubscriber> subscribers, SmartDisplay? config)
    {
        foreach (var subscriber in subscribers)
            _ = clientMessageService.SendAsync(subscriber.Id, OperationType.UpdateSmartDisplayConfig, config);
    }

    private void SendNurseDisplayBroadcast(List<WsSubscriber> subscribers, DisplayNurse? config)
    {
        foreach (var subscriber in subscribers)
            _ = clientMessageService.SendAsync(subscriber.Id, OperationType.UpdateNurseDisplayConfig, config);
    }

    private void SendDisplayBroadcast(Display display, OperationType operation)
    {
        var subscribers = subscribersService.GetSubscribers().Where(s =>
            s.DisplayId == display.Id).ToList();

        switch (operation)
        {
            case OperationType.UpdateDisplayPoC:
                foreach (var subscriber in subscribers)
                    _ = clientMessageService.SendAsync(subscriber.Id, operation, null);
                break;
        }
    }

    #endregion

    #endregion
}