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.MongoModels;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using MongoDB.Bson;

namespace adas_core.Application.Services;

public class NoticeService(
    ILogger<NoticeService> logger,
    ISubscribersService subscribersService,
    INoticeRepository noticeRepository,
    IClientMessageService clientMessageService,
    IDisplayService displayService,
    IHttpContextAccessor httpContextAccessor,
    ILocalAuditService auditService)
    : INoticeService
{
    /// <summary>
    /// Deletes the specified notice by delegating to the ID-based deletion routine.
    /// </summary>
    /// <param name="notice">The notice to delete; its <c>Id</c> is used to identify the record to remove.</param>
    public async Task DeleteNoticeAsync(Notice notice)
    {
        await DeleteNoticeByIdAsync(notice.Id);
    }

    /// <summary>
    /// Deletes a notice identified by its unique ID, creating an audit log entry and broadcasting the deletion when successful. If the notice is not found, the operation is logged and the method returns without making changes; any unexpected exception is logged without being rethrown.
    /// </summary>
    /// <param name="noticeId">The unique identifier of the notice to delete.</param>
    public async Task DeleteNoticeByIdAsync(ObjectId noticeId)
    {
        try
        {
            var noticeAux = await noticeRepository.FindById(noticeId);
            if (noticeAux == null)
            {
                logger.LogInformation("Error deleting Notice not found, id: {noticeId} NOT DELETED  ", noticeId);
                return;
            }

            await noticeRepository.Delete(noticeId);
            await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, noticeAux, null);
            logger.LogInformation("Notice id: {noticeId} DELETED  ", noticeId);

            SendNoticeBroadcast(noticeAux, OperationType.DeleteNotice);
        }
        catch (Exception ex)
        {
            logger.LogError("Exception deleting notice id:{notice} . Exception: {ex}", noticeId, ex);
        }
    }

    /// <summary>
    /// Retrieves a notice by its unique identifier asynchronously. Returns <c>null</c> if an error occurs during the lookup, with the exception being logged.
    /// </summary>
    /// <param name="noticeId">The unique <see cref="ObjectId"/> of the notice to retrieve.</param>
    /// <returns>A <see cref="Notice"/> instance if found; otherwise, <c>null</c> when an exception is thrown during the search.</returns>
    public async Task<Notice?> GetNoticeByIdAsync(ObjectId noticeId)
    {
        try
        {
            return await noticeRepository.FindById(noticeId);
        }
        catch (Exception ex)
        {
            logger.LogError("Exception deleting notice id:{notice} . Exception: {ex}", noticeId, ex);
            return null;
        }
    }

    /// <summary>
    /// Asynchronously retrieves a collection of notices filtered by the specified notice type.
    /// If no notices are found for the given type, a <see cref="NotFoundException"/> is thrown.
    /// </summary>
    /// <param name="noticeType">The type of notice to filter the search by.</param>
    /// <returns>A task that represents the asynchronous operation, containing a collection of <see cref="Notice"/> objects matching the specified type.</returns>
    /// <exception cref="NotFoundException">Thrown when no notices are found for the specified <paramref name="noticeType"/>.</exception>
    public async Task<IEnumerable<Notice>?> GetNoticeByTypeAsync(string noticeType)
    {
        return await noticeRepository.FindByType(noticeType) ??
               throw new NotFoundException(HttpEnum.ErrorMessage.NotFoundResourceMissing);
    }

    /// <summary>
    /// Retrieves all notices from the repository asynchronously.
    /// </summary>
    /// <returns>A task that represents the asynchronous operation. The task result contains a collection of all <see cref="Notice"/> entities.</returns>
    public async Task<IEnumerable<Notice>> GetNoticesAsync()
    {
        return await noticeRepository.FindAll();
    }

    /// <summary>
    /// Inserts a new notice, creates an audit log entry, and broadcasts a notification about the new notice. Returns null if an exception occurs during the operation.
    /// </summary>
    /// <param name="notice">The notice to insert. A new identifier is assigned before persistence.</param>
    /// <returns>The inserted notice with its newly assigned identifier, or null if the operation fails.</returns>
    public async Task<Notice?> InsertNotice(Notice notice)
    {
        try
        {
            notice.Id = new ObjectId();
            await noticeRepository.InsertOneAsync(notice);
            await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, null, notice);
            logger.LogInformation("Notice: {notice} INSERTED", notice);

            SendNoticeBroadcast(notice, OperationType.NewNotice);

            return notice;
        }
        catch (Exception ex)
        {
            logger.LogError("Exception inserting notice {notice} . Exception: {ex}", notice, ex);
            return null;
        }
    }

    /// <summary>
    /// Updates an existing notice, records an audit log entry, and broadcasts the update notification.
    /// If no notice is found by the supplied identifier, a conflict exception is thrown. Errors are caught and logged.
    /// </summary>
    /// <param name="notice">The notice entity containing the updated data.</param>
    /// <exception cref="ConflictException">Thrown when the notice cannot be found in the repository.</exception>
    public async Task UpdateNoticeAsync(Notice notice)
    {
        try
        {
            var auxNotice = await noticeRepository.FindById(notice.Id) ??
                            throw new ConflictException(HttpEnum.ErrorMessage.ConflictUpdateFailed);
            await noticeRepository.Update(notice);
            await auditService.CreateAuditLogAsync(httpContextAccessor.HttpContext?.User!, auxNotice, notice);
            SendNoticeBroadcast(notice, OperationType.UpdateNotice);
        }
        catch (Exception ex)
        {
            logger.LogError("Exception updating notice {notice} . Exception: {ex}", notice, ex);
        }
    }

    /// <summary>
    /// Processes an API request to create, update, or delete a notice based on the request type. Validates required fields for new and updated notices, skips processing when the notice or its description is missing, and logs any exceptions encountered.
    /// </summary>
    /// <param name="apiRequest">The API request containing the notice payload and the operation type (NewNotice, UpdateNotice, or DeleteNotice) to perform.</param>
    public async Task SaveRequest(ApiRequest apiRequest)
    {
        try
        {
            if (apiRequest.Notice == null)
                return;

            switch (apiRequest.Type)
            {
                case "NewNotice":
                    {
                        if (string.IsNullOrEmpty(apiRequest.Notice.Description))
                        {
                            logger.LogDebug("Error saving notice api request. Some values are required. Notice: {notice}",
                                apiRequest.Notice);
                            return;
                        }

                        await InsertNotice(apiRequest.Notice);
                        break;
                    }
                case "UpdateNotice":
                    {
                        if (string.IsNullOrEmpty(apiRequest.Notice.Description))
                        {
                            logger.LogDebug("Error updating notice api request. Some values are required. Notice: {notice}",
                                apiRequest.Notice);
                            return;
                        }

                        await UpdateNoticeAsync(apiRequest.Notice);
                        break;
                    }
                case "DeleteNotice":
                    {
                        await DeleteNoticeAsync(apiRequest.Notice);
                        break;
                    }
            }
        }
        catch (Exception ex)
        {
            logger.LogError("Exception updating notice {notice} . Exception: {ex}", apiRequest.Notice, ex);
        }
    }

    /// <summary>
    /// Asynchronously saves the specified API request by executing the save operation on a background thread.
    /// </summary>
    /// <param name="apiRequest">The API request to save.</param>
    /// <returns>A task that represents the asynchronous save operation.</returns>
    public Task SaveRequestAsync(ApiRequest apiRequest)
    {
        return Task.FromResult(Task.Run(async () => { await SaveRequest(apiRequest); }));
    }

    /// <summary>
    /// Retrieves a collection of notices associated with the specified display identifier.
    /// Returns null if an error occurs while querying the notice repository.
    /// </summary>
    /// <param name="displayId">The ObjectId of the display used to look up associated notices.</param>
    /// <returns>A task containing an <see cref="IEnumerable{T}"/> of <see cref="Notice"/> objects matching the display, or null if the operation fails.</returns>
    public async Task<IEnumerable<Notice>?> GetNoticesByDisplayId(ObjectId displayId)
    {
        try
        {
            return await noticeRepository.FindByDisplayId(displayId);
        }
        catch (Exception e)
        {
            logger.LogError("Unable to get notice by unit on service Exception: {e}", e);
            return null;
        }
    }

    /// <summary>
    /// Asynchronously broadcasts a notice to all subscribers associated with the notice's display.
    /// Logs an error and aborts the broadcast if the display cannot be resolved; any exception thrown while sending is caught and logged.
    /// </summary>
    /// <param name="notice">The notice to broadcast. Its <c>DisplayId</c> is used to resolve the target display and its subscribers.</param>
    /// <param name="operation">The operation type that identifies the kind of notice being broadcast and is forwarded to each subscriber.</param>
    private async void SendNoticeBroadcast(Notice notice, OperationType operation)
    {
        try
        {
            var display = await displayService.GetById(notice.DisplayId);

            if (display == null)
            {
                logger.LogError("Error sending notice broadcast. display is null or empty. Notice: {notice}", notice);
                return;
            }

            var subscribers = subscribersService.GetSubscribers().Where(s => s.DisplayId == display.Id).ToList();

            foreach (var subscriber in subscribers)
                _ = clientMessageService.SendAsync(subscriber.Id, operation, notice);
        }
        catch (Exception ex)
        {
            logger.LogError("Exception sending notice broadcast. Operation type: {op}. Exception: {ex}",
                operation.ToString(), ex);
        }
    }
}