#nullable enable
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Contracts.Reviews;
using Baya.Application.Contracts.Search;
using Baya.Application.Contracts.SupportAlerts;
using Baya.Application.Models.Common;
using Baya.Application.Models.Reviews;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.Reviews;
using Baya.Domain.Entities.SupportAlerts;
using Mediator;

namespace Baya.Application.Features.Reviews.Commands.SubmitReview;

/// <summary>
/// Creates the one allowed review for a completed booking. Guards (all clean <see cref="OperationResult"/>
/// failures, never throws): the caller owns the booking (tenancy), the booking is completed/closed, and no
/// review exists yet (the 1:1 rule, with the UNIQUE index as the race backstop). The AI pre-screen sets the
/// initial disposition — clean text stays <c>pending_moderation</c> by default (the publish gate); the human
/// path can always override later. A rating at/below the configured threshold raises a low-rating support
/// alert reliably (after commit, not swallowed).
/// </summary>
internal sealed class SubmitReviewCommandHandler(
    ICurrentUser currentUser,
    IUnitOfWork unitOfWork,
    IPlatformConfig platformConfig,
    IReviewModerationService moderation,
    ISupportAlertService supportAlerts,
    ISearchIndexMaintainer searchIndex,
    ICacheService cache,
    IDateTimeProvider dateTimeProvider)
    : IRequestHandler<SubmitReviewCommand, OperationResult<SubmitReviewResult>>
{
    public async ValueTask<OperationResult<SubmitReviewResult>> Handle(SubmitReviewCommand request, CancellationToken cancellationToken)
    {
        if (currentUser.UserId is not { } userId)
            return OperationResult<SubmitReviewResult>.UnauthorizedResult("Not authenticated.");

        var customerProfileId = await unitOfWork.CustomerProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken);
        if (customerProfileId is not { } customerId)
            return OperationResult<SubmitReviewResult>.ForbiddenResult("Only a customer can review a booking.");

        var booking = await unitOfWork.ReviewRepository.GetReviewableBookingAsync(request.BookingId, cancellationToken);
        if (booking is null)
            return OperationResult<SubmitReviewResult>.NotFoundResult("Booking not found.");

        // Tenancy: a customer can only review their own booking — a mismatch is a not-found, never a leak.
        if (booking.CustomerProfileId != customerId)
            return OperationResult<SubmitReviewResult>.NotFoundResult("Booking not found.");

        // Eligibility: only a completed/closed booking is reviewable (never cancelled/expired/in-progress).
        if (booking.Status is not (BookingStatus.Completed or BookingStatus.Closed))
            return OperationResult<SubmitReviewResult>.FailureResult("A review can only be left for a completed booking.");

        if (await unitOfWork.ReviewRepository.ExistsForBookingAsync(request.BookingId, cancellationToken))
            return OperationResult<SubmitReviewResult>.ConflictResult("This booking has already been reviewed.");

        var tagMasterIds = new List<long>();
        if (request.TagCodes is { Count: > 0 } codes)
        {
            var distinct = codes.Select(c => c.Trim()).Where(c => c.Length > 0).Distinct().ToList();
            var resolved = await unitOfWork.ReviewRepository.GetTagIdsByCodesAsync(distinct, cancellationToken);
            var unknown = distinct.Where(c => !resolved.ContainsKey(c)).ToList();
            if (unknown.Count > 0)
                return OperationResult<SubmitReviewResult>.FailureResult($"Unknown review tag(s): {string.Join(", ", unknown)}.");
            tagMasterIds.AddRange(resolved.Values);
        }

        // AI pre-screen sets the initial disposition; the mock defaults clean text to a human-review flag so the
        // publish gate holds. Decision authority still rests with ModerateReviewCommand (human override).
        var verdict = await moderation.ScreenAsync(request.Body, cancellationToken);
        var initialStatus = verdict.Decision switch
        {
            ModerationDecision.Approve => ReviewModerationStatus.Published,
            ModerationDecision.Reject => ReviewModerationStatus.Hidden,
            _ => ReviewModerationStatus.PendingModeration
        };

        var now = dateTimeProvider.UtcNow;

        var review = new Review
        {
            BookingId = booking.BookingId,
            CustomerProfileId = customerId,
            NurseProfileId = booking.NurseProfileId,
            Rating = request.Rating,
            Body = string.IsNullOrWhiteSpace(request.Body) ? null : request.Body.Trim()
        };
        foreach (var tagId in tagMasterIds)
            review.TagLinks.Add(new ReviewTagLink { ReviewTagMasterId = tagId });

        if (initialStatus != ReviewModerationStatus.PendingModeration)
            review.Moderate(initialStatus, initialStatus == ReviewModerationStatus.Hidden ? verdict.Reason : null, null, now);

        await unitOfWork.ReviewRepository.AddAsync(review, cancellationToken);

        // A brand-new pending review does not count; an auto-published/auto-hidden one recomputes the aggregate
        // from source in the same transaction (id 0 excludes nothing — the new review is folded in by status).
        if (initialStatus != ReviewModerationStatus.PendingModeration)
            await RecomputeNurseRating.ExecuteAsync(review, unitOfWork, searchIndex, cancellationToken);

        await unitOfWork.CommitAsync();

        if (initialStatus != ReviewModerationStatus.PendingModeration)
            await ReviewCache.InvalidateAggregateAsync(cache, booking.NurseProfileId, cancellationToken);

        // Low-rating safety signal: raise the internal alert reliably (self-committing facade, after the main
        // commit) — never a silently-swallowed best-effort. A raise failure surfaces to the caller.
        var lowRatingAlertRaised = false;
        var threshold = await platformConfig.GetConfig<decimal>("min_rating_for_support_alert", cancellationToken);
        if (request.Rating <= threshold)
        {
            await supportAlerts.RaiseAsync(
                SupportAlertType.LowRating, "review", review.Id.ToString(), SupportAlertSeverity.High,
                bookingId: booking.BookingId, reviewId: review.Id, cancellationToken);
            lowRatingAlertRaised = true;
        }

        return OperationResult<SubmitReviewResult>.SuccessResult(
            new SubmitReviewResult(review.Id, review.ModerationStatus, lowRatingAlertRaised));
    }
}
