#nullable enable
using System.Globalization;
using Baya.Application.Common;
using Baya.Application.Contracts.Common;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Contracts.Search;
using Baya.Application.Models.Catalog;
using Baya.Application.Models.Common;
using Baya.Domain.Entities.Catalog;
using Baya.Domain.Entities.User;
using Mediator;

namespace Baya.Application.Features.Variants.Commands.CreateVariant;

internal sealed class CreateVariantCommandHandler(ICurrentUser currentUser, IUnitOfWork unitOfWork, ISearchIndexMaintainer searchIndex)
    : IRequestHandler<CreateVariantCommand, OperationResult<VariantDto>>
{
    public async ValueTask<OperationResult<VariantDto>> Handle(CreateVariantCommand request, CancellationToken cancellationToken)
    {
        if (currentUser.UserId is not { } userId)
            return OperationResult<VariantDto>.UnauthorizedResult("Not authenticated.");

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

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

        // Catalog must be seeded and the category active — a variant can't rest on a missing/inactive category.
        var category = await unitOfWork.CatalogRepository.GetActiveCategoryAsync(request.ServiceCategoryId, cancellationToken);
        if (category is null)
            return OperationResult<VariantDto>.FailureResult(nameof(request.ServiceCategoryId), "Category not found or inactive.");

        // Applicable = the category's own active groups PLUS every cross-category (NULL) active group.
        var applicableGroups = await unitOfWork.CatalogRepository.GetApplicableGroupsAsync(request.ServiceCategoryId, cancellationToken);
        var groupById = applicableGroups.ToDictionary(g => g.Id);

        // Every submitted (group, value) must apply to this category and the value must belong to its group.
        foreach (var selection in request.Options)
        {
            if (!groupById.TryGetValue(selection.OptionGroupId, out var group))
                return OperationResult<VariantDto>.FailureResult(
                    nameof(request.Options), $"Option group {selection.OptionGroupId} does not apply to this category.");

            if (group.Values.All(v => v.Id != selection.OptionValueId))
                return OperationResult<VariantDto>.FailureResult(
                    nameof(request.Options), $"Option value {selection.OptionValueId} is not a valid choice for '{group.NameFa}'.");
        }

        // One value per dimension: no group answered twice.
        var answeredGroupIds = request.Options.Select(o => o.OptionGroupId).ToList();
        if (answeredGroupIds.Count != answeredGroupIds.Distinct().Count())
            return OperationResult<VariantDto>.FailureResult(nameof(request.Options), "A dimension was answered more than once.");

        // Every required dimension (incl. cross-category ones) must be answered.
        var answered = answeredGroupIds.ToHashSet();
        var missingRequired = applicableGroups.Where(g => g.IsRequired && !answered.Contains(g.Id)).ToList();
        if (missingRequired.Count > 0)
            return OperationResult<VariantDto>.FailureResult(
                nameof(request.Options),
                $"Required dimension(s) not answered: {string.Join(", ", missingRequired.Select(g => g.NameFa))}.");

        var resolvedOptions = ResolveOptions(request.Options, groupById);
        var optionSetHash = OptionSetHash.Compute(request.Options.Select(o => (o.OptionGroupId, o.OptionValueId)));

        // Duplicate-listing guard: friendly pre-check ahead of the filtered unique-index DB backstop.
        if (await unitOfWork.NurseServiceVariantRepository.DuplicateHashExistsAsync(nid, category.Id, optionSetHash, null, cancellationToken))
            return OperationResult<VariantDto>.ConflictResult("You already offer this exact configuration in this category.");

        var price = long.Parse(request.Price, NumberStyles.None, CultureInfo.InvariantCulture);
        var displayName = string.IsNullOrWhiteSpace(request.DisplayName)
            ? BuildDisplayName(category.NameFa, resolvedOptions)
            : request.DisplayName.Trim();

        var variant = new NurseServiceVariant
        {
            NurseId = nid,
            ServiceCategoryId = category.Id,
            Price = price,
            PriceUnit = request.PriceUnit,
            SessionCount = request.SessionCount,
            DisplayName = displayName,
            OptionSetHash = optionSetHash,
            IsActive = true,
            Options = request.Options
                .Select(o => new NurseServiceVariantOption
                {
                    OptionGroupId = o.OptionGroupId,
                    OptionValueId = o.OptionValueId
                })
                .ToList()
        };

        await unitOfWork.NurseServiceVariantRepository.AddAsync(variant, cancellationToken);

        // Fan this variant out into nurse_search_index across the nurse's service areas, in the same unit of
        // work — the new variant's generated id is assigned to its index rows on the single commit below.
        await searchIndex.ReindexVariantAsync(variant, cancellationToken);
        await unitOfWork.CommitAsync();

        return OperationResult<VariantDto>.SuccessResult(new VariantDto(
            variant.Id,
            category.Id,
            category.NameFa,
            category.NameEn,
            variant.Price.ToString(CultureInfo.InvariantCulture),
            variant.PriceUnit,
            variant.SessionCount,
            variant.DisplayName,
            variant.IsActive,
            resolvedOptions));
    }

    private static IReadOnlyList<VariantOptionDto> ResolveOptions(
        IReadOnlyList<VariantOptionSelection> selections,
        IReadOnlyDictionary<long, OptionGroupDto> groupById)
        => selections
            .Select(s =>
            {
                var group = groupById[s.OptionGroupId];
                var value = group.Values.First(v => v.Id == s.OptionValueId);
                return (group, value);
            })
            .OrderBy(x => x.group.SortOrder)
            .ThenBy(x => x.group.Id)
            .Select(x => new VariantOptionDto(
                x.group.Id, x.group.NameFa, x.group.NameEn, x.value.Id, x.value.NameFa, x.value.NameEn))
            .ToList();

    private static string BuildDisplayName(string categoryNameFa, IReadOnlyList<VariantOptionDto> options)
        => options.Count == 0
            ? categoryNameFa
            : $"{categoryNameFa} · {string.Join(" · ", options.Select(o => o.ValueNameFa))}";
}
