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

namespace Baya.Application.Features.Bookings.Commands.CheckInVisit;

internal sealed class CheckInVisitCommandHandler(
    ICurrentUser currentUser,
    IUnitOfWork unitOfWork,
    IPlatformConfig platformConfig,
    IDateTimeProvider dateTimeProvider,
    IGeocoder geocoder,
    ISupportAlertService supportAlerts,
    INotificationDispatcher notifications)
    : IRequestHandler<CheckInVisitCommand, OperationResult<VisitVerificationDto>>
{
    private static readonly JsonSerializerOptions SnapshotJson = new() { PropertyNameCaseInsensitive = true };

    public async ValueTask<OperationResult<VisitVerificationDto>> Handle(CheckInVisitCommand request, CancellationToken cancellationToken)
    {
        if (currentUser.UserId is not { } userId)
            return OperationResult<VisitVerificationDto>.UnauthorizedResult("Not authenticated.");

        if (currentUser.Roles?.Contains(RoleNames.Nurse) != true)
            return OperationResult<VisitVerificationDto>.ForbiddenResult("Only a nurse can check in.");

        var nurseId = await unitOfWork.NurseProfileRepository.GetProfileIdByUserIdAsync(userId, cancellationToken);
        if (nurseId is not { } nid)
            return OperationResult<VisitVerificationDto>.ForbiddenResult("No nurse profile exists yet.");

        var booking = await unitOfWork.BookingRepository.GetTrackedBookingBySessionAsync(request.SessionId, cancellationToken);
        var session = booking?.Sessions.FirstOrDefault(s => s.Id == request.SessionId);
        if (booking is null || session is null)
            return OperationResult<VisitVerificationDto>.NotFoundResult("Session not found.");

        if (booking.NurseId != nid)
            return OperationResult<VisitVerificationDto>.NotFoundResult("Session not found.");

        if (booking.Status is not (BookingStatus.Confirmed or BookingStatus.InProgress))
            return OperationResult<VisitVerificationDto>.ConflictResult("This booking is not in a state where a visit can start.");

        if (!session.CanTransitionTo(BookingSessionStatus.InProgress))
            return OperationResult<VisitVerificationDto>.ConflictResult("This session cannot be checked in.");

        var now = dateTimeProvider.UtcNow.UtcDateTime;

        var verification = session.Verification ?? new VisitVerification { BookingSessionId = session.Id };
        session.Verification = verification;

        verification.CheckInAt = now;
        verification.CheckInLat = request.Latitude;
        verification.CheckInLng = request.Longitude;

        // Advisory address match — computed only when GPS is present. GPS-denied still checks in (flagged null).
        bool mismatch = false;
        if (request.Latitude is { } lat && request.Longitude is { } lng)
        {
            var addressPoint = await ResolveAddressPointAsync(booking.AddressSnapshotJson, cancellationToken);
            if (addressPoint is { } point)
            {
                var toleranceMeters = await platformConfig.GetConfig<int>("evv_location_tolerance_meters", cancellationToken);
                var distance = GeoDistance.HaversineMeters((double)point.Lat, (double)point.Lng, (double)lat, (double)lng);

                verification.CheckInDistanceMeters = (decimal)Math.Round(distance, 2);
                verification.CheckInAddressMatch = distance <= toleranceMeters;
                mismatch = distance > toleranceMeters;
            }
        }

        verification.MarkCheckedIn();
        session.TransitionTo(BookingSessionStatus.InProgress);

        // First relevant check-in moves the booking to in_progress; subsequent check-ins leave it as-is.
        if (booking.CanTransitionTo(BookingStatus.InProgress))
            booking.TransitionTo(BookingStatus.InProgress, now);

        await unitOfWork.CommitAsync();

        // A mismatch is advisory: raise an admin alert + notify the family — never block, never cancel.
        if (mismatch)
        {
            await supportAlerts.RaiseAsync(
                SupportAlertType.EvvLocationMismatch,
                entityType: "booking_session",
                entityId: session.Id.ToString(),
                severity: SupportAlertSeverity.Medium,
                bookingId: booking.Id,
                cancellationToken: cancellationToken);

            var participants = await unitOfWork.BookingRepository.GetParticipantsAsync(booking.Id, cancellationToken);
            if (participants is not null)
                await notifications.DispatchAsync(
                    new Notification(
                        participants.CustomerUserId,
                        "evv_location_mismatch",
                        "Check-in location flagged",
                        "The nurse checked in outside the expected area. Our team will review it — the visit continues normally.",
                        JsonSerializer.Serialize(new { booking_id = booking.Id, session_id = session.Id })),
                    cancellationToken);
        }

        var gate = await unitOfWork.BookingRepository.GetEvvForSessionAsync(session.Id, cancellationToken);
        return OperationResult<VisitVerificationDto>.SuccessResult(gate!.Evv!);
    }

    // The booking address is frozen in the (decrypted) snapshot; reuse IGeocoder to resolve its coordinates,
    // falling back to the coordinates already frozen into the snapshot. Returns null when neither is available.
    private async Task<(decimal Lat, decimal Lng)?> ResolveAddressPointAsync(string addressSnapshotJson, CancellationToken cancellationToken)
    {
        AddressSnapshotJsonModel? snapshot;
        try
        {
            snapshot = JsonSerializer.Deserialize<AddressSnapshotJsonModel>(addressSnapshotJson, SnapshotJson);
        }
        catch (JsonException)
        {
            snapshot = null;
        }

        if (snapshot is null)
            return null;

        var geo = await geocoder.GeocodeAsync(
            snapshot.AddressLine ?? string.Empty,
            snapshot.CityNameEn ?? string.Empty,
            snapshot.DistrictNameEn,
            cancellationToken);

        if (geo.Latitude is { } gLat && geo.Longitude is { } gLng)
            return (gLat, gLng);

        if (snapshot.Latitude is { } sLat && snapshot.Longitude is { } sLng)
            return (sLat, sLng);

        return null;
    }

    private sealed record AddressSnapshotJsonModel(
        string? AddressLine,
        string? CityNameEn,
        string? DistrictNameEn,
        decimal? Latitude,
        decimal? Longitude);
}
