#nullable enable
using System.Text.Json;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Configuration;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Models.Booking;
using Baya.Application.Models.Common;
using Baya.Domain.Entities.Booking;
using Baya.Domain.Entities.User;
using Mediator;

namespace Baya.Application.Features.Booking.Commands.CreateBookingRequest;

internal sealed class CreateBookingRequestCommandHandler(
    ICurrentUser currentUser,
    IUnitOfWork unitOfWork,
    IPlatformConfig platformConfig,
    IDateTimeProvider dateTimeProvider,
    INotificationDispatcher notifications)
    : IRequestHandler<CreateBookingRequestCommand, OperationResult<BookingRequestDto>>
{
    public async ValueTask<OperationResult<BookingRequestDto>> Handle(CreateBookingRequestCommand request, CancellationToken cancellationToken)
    {
        if (currentUser.UserId is not { } userId)
            return OperationResult<BookingRequestDto>.UnauthorizedResult("Not authenticated.");

        if (currentUser.Roles?.Contains(RoleNames.Customer) != true)
            return OperationResult<BookingRequestDto>.ForbiddenResult("Only a customer can create a booking request.");

        // Tenancy anchor: the customer id is resolved from the caller, never trusted from the body.
        var customerId = await unitOfWork.CustomerProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken);
        if (customerId is not { } cid)
            return OperationResult<BookingRequestDto>.FailureResult("No customer profile exists yet. Create your profile first.");

        // Tenancy invariant: patient + address must belong to the caller; the variant must belong to the
        // requested nurse. A mismatch is a clean not-found — never a 500, never a leak of another party's row.
        var patient = await unitOfWork.PatientRepository.GetOwnedAsync(request.PatientId, cid, cancellationToken);
        if (patient is null)
            return OperationResult<BookingRequestDto>.NotFoundResult("Patient not found.");

        var address = await unitOfWork.CustomerAddressRepository.GetOwnedAsync(request.CustomerAddressId, cid, cancellationToken);
        if (address is null)
            return OperationResult<BookingRequestDto>.NotFoundResult("Address not found.");

        var variant = await unitOfWork.NurseServiceVariantRepository.GetOwnedAsync(request.VariantId, request.NurseId, cancellationToken);
        if (variant is null)
            return OperationResult<BookingRequestDto>.NotFoundResult("Service variant not found for this nurse.");

        if (!variant.IsActive)
            return OperationResult<BookingRequestDto>.FailureResult(nameof(request.VariantId), "This service variant is not currently offered.");

        var nurse = await unitOfWork.NurseProfileRepository.GetBookingContextByIdAsync(request.NurseId, cancellationToken);
        if (nurse is null)
            return OperationResult<BookingRequestDto>.NotFoundResult("Nurse not found.");

        if (!nurse.IsVerified || !nurse.IsAcceptingBookings)
            return OperationResult<BookingRequestDto>.FailureResult(nameof(request.NurseId), "This nurse is not currently accepting bookings.");

        // Same-gender care is decisive for bodily care — a male/female requirement must match the nurse's
        // gender at request time; "any" matches either. Never defaulted, never advisory.
        if (!CaregiverGender.Matches(request.RequiredCaregiverGender, nurse.Gender ?? string.Empty))
            return OperationResult<BookingRequestDto>.FailureResult(
                nameof(request.RequiredCaregiverGender),
                $"This nurse's gender does not match the required caregiver gender '{request.RequiredCaregiverGender}'.");

        var now = dateTimeProvider.UtcNow.UtcDateTime;
        if (request.RequestedDate < DateOnly.FromDateTime(now))
            return OperationResult<BookingRequestDto>.FailureResult(nameof(request.RequestedDate), "The requested date cannot be in the past.");

        // Deadline is read from config and FROZEN as an absolute timestamp so a later config change can never
        // move an existing request's deadline.
        var responseDeadlineHours = await platformConfig.GetConfig<int>("nurse_response_deadline_hours", cancellationToken);

        var bookingRequest = new BookingRequest
        {
            CustomerId = cid,
            NurseId = request.NurseId,
            PatientId = request.PatientId,
            VariantId = request.VariantId,
            CustomerAddressId = request.CustomerAddressId,
            RequiredCaregiverGender = request.RequiredCaregiverGender,
            RequestedDate = request.RequestedDate,
            RequestedTimeStart = request.RequestedTimeStart,
            RequestedTimeEnd = request.RequestedTimeEnd,
            CustomerNotes = string.IsNullOrWhiteSpace(request.CustomerNotes) ? null : request.CustomerNotes.Trim(),
            NurseResponseDeadlineAt = now.AddHours(responseDeadlineHours)
        };

        await unitOfWork.BookingRequestRepository.AddAsync(bookingRequest, cancellationToken);
        await unitOfWork.CommitAsync();

        // The dispatcher self-commits its own row — invoke it only AFTER the request is persisted. Stage-1
        // disclosure: the payload carries the patient display name + date, nothing more.
        await notifications.DispatchAsync(
            new Notification(
                nurse.UserId,
                "booking_request_received",
                "New booking request",
                $"A new booking request from a customer awaits your response.",
                JsonSerializer.Serialize(new
                {
                    booking_request_id = bookingRequest.Id,
                    patient_display_name = patient.DisplayName,
                    requested_date = request.RequestedDate.ToString("O")
                })),
            cancellationToken);

        var detail = await unitOfWork.BookingRequestRepository.GetDetailAsync(bookingRequest.Id, cancellationToken);
        // The creator is the owning customer, so they see their own full address.
        return OperationResult<BookingRequestDto>.SuccessResult(BookingRequestMapper.ToDto(detail!, includeFullAddress: true));
    }
}
