using Baya.Domain.Common;
using Baya.Domain.Entities.Catalog;

namespace Baya.Domain.Entities.Search;

/// <summary>
/// The denormalized, maintained-on-write read model behind nurse discovery (b7): <b>one flat row per
/// (bookable variant × covered service area)</b>. It flattens facts that otherwise live across four
/// domains — the variant's category/price (catalog), the covered city/district (geography), the nurse's
/// gender + rating aggregates (identity), and the verification-derived bookability — so a family search is
/// a single indexed, paginated scan instead of a 4+ table join with a rating sort.
/// <para>
/// This is a <b>read-only projection</b>: it is written <i>only</i> by the search-index maintainer, which
/// re-derives every field from the source tables. Never let a search read mutate it, and never treat it as
/// the source of truth for anything.
/// </para>
/// <para>
/// <see cref="IsSearchable"/> is the single visibility gate — a row is returned to families only when it is
/// <c>true</c>, which holds <b>only</b> when the nurse is verified, not suspended, accepting bookings, and
/// the variant is active (see the maintainer). <see cref="DistrictId"/> == <c>null</c> is a meaningful
/// "whole city" coverage value, not missing data. <see cref="Price"/> is IRR Rials as an integer — no float.
/// </para>
/// </summary>
public class NurseSearchIndex : BaseEntity<long>
{
    public long VariantId { get; set; }

    /// <summary>Reference navigation so a row projected for a freshly-created variant is inserted in the same
    /// graph — EF assigns the generated <c>variant_id</c> in one <c>SaveChanges</c>.</summary>
    public NurseServiceVariant Variant { get; set; }

    public long NurseId { get; set; }

    public long ServiceCategoryId { get; set; }

    /// <summary>IRR Rials, integer — copied from the variant. No float money path, ever.</summary>
    public long Price { get; set; }

    /// <summary>Closed code set (see <see cref="PriceUnits"/>) — copied from the variant.</summary>
    public string PriceUnit { get; set; }

    public long CityId { get; set; }

    /// <summary>NULL = "whole city" — a deliberate coverage value, not missing data. A city search matches
    /// both NULL-district rows and any district row in the city; a district search matches that district's
    /// rows plus the NULL-district (whole-city) rows.</summary>
    public long? DistrictId { get; set; }

    /// <summary>Copied from <c>users.gender</c> via the nurse, for the first-class same-gender filter.</summary>
    public string NurseGender { get; set; }

    /// <summary>The nurse's display name, denormalized from <c>users</c> so the discovery card renders a
    /// real, named person without a per-row join. Recomputed on every nurse-level reindex.</summary>
    public string NurseName { get; set; }

    /// <summary>The nurse's avatar URL, denormalized from <c>nurse_profiles</c>. NULL when the nurse has
    /// not uploaded a photo (the card falls back to initials).</summary>
    public string AvatarUrl { get; set; }

    public decimal AverageRating { get; set; }
    public int TotalReviews { get; set; }
    public int TotalCompletedBookings { get; set; }

    /// <summary>The single visibility gate: <c>true</c> only when nurse <c>is_verified=1</c> AND not
    /// suspended AND <c>is_accepting_bookings=1</c> AND variant <c>is_active=1</c>. Recomputed on every
    /// relevant source write — never trusted as a stale value.</summary>
    public bool IsSearchable { get; set; }

    /// <summary>Stamped from <see cref="IDateTimeProvider"/> on every upsert.</summary>
    public DateTimeOffset UpdatedAt { get; set; }

    public DateTimeOffset? DeletedAt { get; set; }
}
