#nullable enable
using System.Globalization;
using System.Linq.Expressions;
using Baya.Application.Contracts.Persistence;
using Baya.Application.Models.Catalog;
using Baya.Application.Models.Common;
using Baya.Domain.Entities.Catalog;
using Baya.Infrastructure.Persistence.Repositories.Common;
using Microsoft.EntityFrameworkCore;

namespace Baya.Infrastructure.Persistence.Repositories;

internal sealed class NurseServiceVariantRepository : BaseAsyncRepository<NurseServiceVariant>, INurseServiceVariantRepository
{
    public NurseServiceVariantRepository(ApplicationDbContext dbContext) : base(dbContext)
    {
    }

    public Task AddAsync(NurseServiceVariant variant, CancellationToken cancellationToken)
        => base.AddAsync(variant);

    public Task<NurseServiceVariant?> GetOwnedAsync(long id, long nurseId, CancellationToken cancellationToken)
        => Table.FirstOrDefaultAsync(v => v.Id == id && v.NurseId == nurseId, cancellationToken);

    public Task<bool> DuplicateHashExistsAsync(long nurseId, long serviceCategoryId, string optionSetHash, long? excludeVariantId, CancellationToken cancellationToken)
        => TableNoTracking.AnyAsync(
            v => v.NurseId == nurseId
                 && v.ServiceCategoryId == serviceCategoryId
                 && v.OptionSetHash == optionSetHash
                 && (excludeVariantId == null || v.Id != excludeVariantId),
            cancellationToken);

    public async Task<PagedResult<VariantDto>> ListMineAsync(long nurseId, int page, int pageSize, CancellationToken cancellationToken)
    {
        var query = TableNoTracking.Where(v => v.NurseId == nurseId);

        var total = await query.CountAsync(cancellationToken);
        var rows = await query
            // Active offerings first, then newest — the deactivated ones stay visibly distinct at the tail.
            .OrderByDescending(v => v.IsActive)
            .ThenByDescending(v => v.Id)
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .Select(Projection)
            .ToListAsync(cancellationToken);

        return new PagedResult<VariantDto>(rows.Select(Map).ToList(), total, page, pageSize);
    }

    public async Task<VariantDto?> GetOwnedProjectedAsync(long id, long nurseId, CancellationToken cancellationToken)
    {
        var row = await TableNoTracking
            .Where(v => v.Id == id && v.NurseId == nurseId)
            .Select(Projection)
            .FirstOrDefaultAsync(cancellationToken);
        return row is null ? null : Map(row);
    }

    public async Task<VariantDto?> GetProjectedAsync(long id, CancellationToken cancellationToken)
    {
        var row = await TableNoTracking
            .Where(v => v.Id == id)
            .Select(Projection)
            .FirstOrDefaultAsync(cancellationToken);
        return row is null ? null : Map(row);
    }

    public async Task<VariantDto?> GetPublicProjectedAsync(long id, CancellationToken cancellationToken)
    {
        var row = await TableNoTracking
            .Where(v => v.Id == id && v.IsActive)
            .Select(Projection)
            .FirstOrDefaultAsync(cancellationToken);
        return row is null ? null : Map(row);
    }

    // Shared DB projection: keeps Price as the raw long (translatable) and resolves category/option labels.
    // Price is formatted to a digit string in memory (see Map) so no long.ToString() SQL translation is
    // required, and the option-set is a single-level collection projection (SQLite-safe).
    private static readonly Expression<Func<NurseServiceVariant, VariantRow>> Projection = v => new VariantRow(
        v.Id,
        v.ServiceCategoryId,
        v.ServiceCategory.NameFa,
        v.ServiceCategory.NameEn,
        v.Price,
        v.PriceUnit,
        v.SessionCount,
        v.DisplayName,
        v.IsActive,
        v.Options
            .OrderBy(o => o.OptionGroup.SortOrder)
            .ThenBy(o => o.OptionGroupId)
            .Select(o => new VariantOptionDto(
                o.OptionGroupId,
                o.OptionGroup.NameFa,
                o.OptionGroup.NameEn,
                o.OptionValueId,
                o.OptionValue.NameFa,
                o.OptionValue.NameEn))
            .ToList());

    private static VariantDto Map(VariantRow r) => new(
        r.Id,
        r.ServiceCategoryId,
        r.CategoryNameFa,
        r.CategoryNameEn,
        r.Price.ToString(CultureInfo.InvariantCulture),
        r.PriceUnit,
        r.SessionCount,
        r.DisplayName,
        r.IsActive,
        r.Options);

    private sealed record VariantRow(
        long Id,
        long ServiceCategoryId,
        string CategoryNameFa,
        string CategoryNameEn,
        long Price,
        string PriceUnit,
        int? SessionCount,
        string DisplayName,
        bool IsActive,
        List<VariantOptionDto> Options);
}
