#nullable enable
using Baya.Application.Common;
using Baya.Application.Contracts.Audit;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Contracts.Search;
using Baya.Application.Models.Common;
using Baya.Application.Models.Verification;
using Baya.Domain.Entities.Verification;
using Mediator;

namespace Baya.Application.Features.Verification.Commands.ReviewStep;

internal sealed class AdminReviewStepCommandHandler(
    ICurrentUser currentUser,
    IUnitOfWork unitOfWork,
    ICredentialVerifier credentialVerifier,
    IAuditLogger auditLogger,
    ICacheService cache,
    IDateTimeProvider dateTimeProvider,
    ISearchIndexMaintainer searchIndex)
    : IRequestHandler<AdminReviewStepCommand, OperationResult<ReviewStepResult>>
{
    public async ValueTask<OperationResult<ReviewStepResult>> Handle(AdminReviewStepCommand request, CancellationToken cancellationToken)
    {
        if (currentUser.UserId is not { } adminId)
            return OperationResult<ReviewStepResult>.UnauthorizedResult("Not authenticated.");

        var repo = unitOfWork.VerificationRepository;

        var step = await repo.GetTrackedStepWithVerificationAsync(request.StepId, cancellationToken);
        if (step is null)
            return OperationResult<ReviewStepResult>.NotFoundResult("Verification step not found.");

        if (step.IsAutomated)
            return OperationResult<ReviewStepResult>.FailureResult("Automated steps are decided by their vendor run, not manual review.");

        var verification = step.NurseVerification;
        var nurseId = verification.NurseId;
        var now = dateTimeProvider.UtcNow;
        NurseCredential? recordedCredential = null;

        if (request.Approve)
        {
            if (VerificationStepTypeCodes.CredentialBearing.Contains(step.StepType.Code))
            {
                var credentialResult = await RecordCredentialAsync(request, step, nurseId, adminId, repo, cancellationToken);
                if (!credentialResult.IsSuccess)
                    return OperationResult<ReviewStepResult>.FailureResult(FirstError(credentialResult));

                recordedCredential = credentialResult.Result;
            }

            step.Status = VerificationStepStatus.Passed;
            step.FailureReason = null;
            step.CompletedAt = now;
        }
        else
        {
            step.Status = VerificationStepStatus.Failed;
            step.FailureReason = request.RejectionReason;
            step.CompletedAt = now;
            verification.RejectionReason = request.RejectionReason;
        }

        verification.ReviewedByAdminId = adminId;

        var profile = await unitOfWork.NurseProfileRepository.GetTrackedByIdAsync(nurseId, cancellationToken);
        if (profile is null)
            return OperationResult<ReviewStepResult>.NotFoundResult("Nurse profile not found.");

        VerificationAggregator.Finalize(verification, profile, now);

        // Any is_verified flip must reach the search projection in the same transaction — a newly-verified
        // nurse's rows become searchable; a rejection reverses it.
        await searchIndex.ReindexNurseAsync(profile, verification.Status, cancellationToken);

        // The step decision, the recorded credential, and any is_verified flip land in one transaction.
        await unitOfWork.CommitAsync();

        await auditLogger.WriteAsync(
            "verification_step",
            step.Id.ToString(),
            request.Approve ? "approve" : "reject",
            new Dictionary<string, object?>
            {
                ["step_code"] = step.StepType.Code,
                ["decision"] = request.Approve ? "passed" : "failed",
                ["admin_id"] = adminId,
                ["reason"] = request.Approve ? null : request.RejectionReason
            },
            cancellationToken);

        await VerificationCache.InvalidateBadgeAsync(cache, nurseId, cancellationToken);

        return OperationResult<ReviewStepResult>.SuccessResult(new ReviewStepResult(step.Id, step.Status.ToCode(), recordedCredential?.Id));
    }

    private async ValueTask<OperationResult<NurseCredential>> RecordCredentialAsync(
        AdminReviewStepCommand request,
        VerificationStep step,
        long nurseId,
        int adminId,
        IVerificationRepository repo,
        CancellationToken cancellationToken)
    {
        if (string.IsNullOrWhiteSpace(request.CredentialNumber)
            || string.IsNullOrWhiteSpace(request.HolderName)
            || string.IsNullOrWhiteSpace(request.IssuingAuthority))
        {
            return OperationResult<NurseCredential>.FailureResult("Credential number, holder name and issuing authority are required to approve this step.");
        }

        // The criminal-record certificate is time-limited — an expiry is mandatory and drives the re-scan.
        if (step.StepType.Code == VerificationStepTypeCodes.CriminalRecord && request.ExpiresAt is null)
            return OperationResult<NurseCredential>.FailureResult("An expiry date is required for the criminal-record certificate.");

        // Anti-forgery gate: the printed holder name must match the verified identity before we record it.
        var identityName = await repo.GetNurseIdentityNameAsync(nurseId, cancellationToken);
        if (!IdentityNameMatch.Matches(identityName, request.HolderName))
            return OperationResult<NurseCredential>.FailureResult("The credential holder name does not match the verified identity — it cannot be recorded.");

        var check = await credentialVerifier.VerifyAsync(step.StepType.Code, request.CredentialNumber, cancellationToken);

        var credential = new NurseCredential
        {
            NurseId = nurseId,
            CredentialType = step.StepType.Code,
            CredentialNumber = request.CredentialNumber!.Trim(),
            HolderNameSnapshot = request.HolderName!.Trim(),
            IssuingAuthority = request.IssuingAuthority!.Trim(),
            IssuedAt = request.IssuedAt,
            ExpiresAt = request.ExpiresAt,
            VerificationSource = request.VerificationSource,
            VerificationMethod = check.Method,
            VerifiedByAdminId = adminId
        };
        await repo.AddCredentialAsync(credential, cancellationToken);

        // Mirror the credential expiry onto the (time-limited) step so the scanner can re-gate on it. Valid
        // through the whole expiry day → the step lapses at the start of the following day.
        if (request.ExpiresAt is { } expires)
            step.ExpiresAt = new DateTimeOffset(expires.AddDays(1).ToDateTime(TimeOnly.MinValue), TimeSpan.Zero);

        return OperationResult<NurseCredential>.SuccessResult(credential);
    }

    private static string FirstError(IOperationResult result)
        => result.ErrorMessages.Count > 0 ? result.ErrorMessages[0].Value : "Credential could not be recorded.";
}
