#nullable enable
using Baya.Application.Common;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Models.Common;
using Baya.Application.Models.Messaging;
using Baya.Domain.Entities.Messaging;
using Mediator;

namespace Baya.Application.Features.Messaging.Commands.PostMessage;

/// <summary>
/// Appends a message to a ticket after enforcing the visibility + posting rules: only an active participant (or
/// staff) may post; <c>is_internal</c> is staff-only (the hard boundary); a non-staff caller cannot post to a
/// closed ticket. A non-internal message notifies the other active participants (in-app only at MVP).
/// </summary>
internal sealed class PostMessageCommandHandler(
    ICurrentUser currentUser,
    IUnitOfWork unitOfWork,
    IDateTimeProvider dateTimeProvider,
    INotificationDispatcher notifications)
    : IRequestHandler<PostMessageCommand, OperationResult<PostMessageResult>>
{
    public async ValueTask<OperationResult<PostMessageResult>> Handle(PostMessageCommand request, CancellationToken cancellationToken)
    {
        if (currentUser.UserId is not { } userId)
            return OperationResult<PostMessageResult>.UnauthorizedResult("Not authenticated.");

        var isStaff = StaffRoles.IsStaff(currentUser.Roles);

        var header = await unitOfWork.TicketRepository.GetHeaderAsync(request.TicketId, cancellationToken);
        if (header is null)
            return OperationResult<PostMessageResult>.NotFoundResult("Ticket not found.");

        if (!isStaff && !await unitOfWork.TicketRepository.IsActiveParticipantAsync(request.TicketId, userId, cancellationToken))
            return OperationResult<PostMessageResult>.ForbiddenResult("You are not a participant on this ticket.");

        if (request.IsInternal && !isStaff)
            return OperationResult<PostMessageResult>.ForbiddenResult("Only staff can post an internal note.");

        if (header.Status == TicketStatus.Closed && !isStaff)
            return OperationResult<PostMessageResult>.ForbiddenResult("This ticket is closed.");

        // Optimistic-send idempotency: a retried post with the same client message id returns the original
        // message instead of a duplicate.
        if (!string.IsNullOrWhiteSpace(request.ClientMessageId))
        {
            var existing = await unitOfWork.TicketRepository.GetMessageByClientIdAsync(request.TicketId, request.ClientMessageId, cancellationToken);
            if (existing is not null)
                return OperationResult<PostMessageResult>.SuccessResult(
                    new PostMessageResult(existing.Id, request.TicketId, existing.SentAt, request.ClientMessageId));
        }

        var now = dateTimeProvider.UtcNow;
        var message = new TicketMessage
        {
            TicketId = request.TicketId,
            SenderId = userId,
            Body = request.Body,
            IsInternal = request.IsInternal,
            ClientMessageId = string.IsNullOrWhiteSpace(request.ClientMessageId) ? null : request.ClientMessageId,
            SentAt = now
        };

        await unitOfWork.TicketRepository.AddMessageAsync(message, cancellationToken);
        await unitOfWork.CommitAsync();

        // Notify the other active participants — but never for an internal note (it must not surface to users).
        if (!message.IsInternal)
        {
            var recipients = await unitOfWork.TicketRepository.GetActiveParticipantUserIdsAsync(request.TicketId, cancellationToken);
            foreach (var recipient in recipients.Where(r => r != userId))
                await notifications.DispatchAsync(
                    new Notification(recipient, "ticket_message",
                        "New ticket message", $"New message on ticket {header.ReferenceCode}.",
                        $"{{\"ticketId\":{request.TicketId},\"referenceCode\":\"{header.ReferenceCode}\"}}"),
                    cancellationToken);
        }

        return OperationResult<PostMessageResult>.SuccessResult(
            new PostMessageResult(message.Id, request.TicketId, message.SentAt, message.ClientMessageId));
    }
}
