namespace SIGCM2.Domain.Entities;

/// <summary>
/// Immutable product entity for the commercial catalog.
/// Factory method ForCreation creates new products (Id=0).
/// Mutation methods (With*) return new instances — original is never modified.
/// Flag coherence (RequiresCategory/HasDuration) is enforced by Application handlers
/// at creation/update time against the ProductType, NOT here in the entity.
/// MedioId and ProductTypeId are immutable post-creation by design.
/// </summary>
public sealed class Product
{
    private const int NombreMaxLength = 300;

    public int Id { get; }
    public string Nombre { get; }
    public int MedioId { get; }
    public int ProductTypeId { get; }
    public int? RubroId { get; }
    public decimal BasePrice { get; }
    public int? PriceDurationDays { get; }
    public bool IsActive { get; }
    public DateTime FechaCreacion { get; }
    public DateTime? FechaModificacion { get; }

    /// <summary>Full hydration constructor — used by the repository to reconstruct from DB rows.</summary>
    public Product(
        int id,
        string nombre,
        int medioId,
        int productTypeId,
        int? rubroId,
        decimal basePrice,
        int? priceDurationDays,
        bool isActive,
        DateTime fechaCreacion,
        DateTime? fechaModificacion)
    {
        Id = id;
        Nombre = nombre;
        MedioId = medioId;
        ProductTypeId = productTypeId;
        RubroId = rubroId;
        BasePrice = basePrice;
        PriceDurationDays = priceDurationDays;
        IsActive = isActive;
        FechaCreacion = fechaCreacion;
        FechaModificacion = fechaModificacion;
    }

    /// <summary>
    /// Factory for a new Product. Id=0 — DB assigns via IDENTITY.
    /// IsActive=true, FechaModificacion=null.
    /// </summary>
    public static Product ForCreation(
        string nombre,
        int medioId,
        int productTypeId,
        int? rubroId,
        decimal basePrice,
        int? priceDurationDays,
        TimeProvider timeProvider)
    {
        ValidateNombre(nombre);
        ValidateMedioId(medioId);
        ValidateProductTypeId(productTypeId);
        ValidateRubroId(rubroId);
        ValidateBasePrice(basePrice);
        ValidatePriceDurationDays(priceDurationDays);

        return new Product(
            id: 0,
            nombre: nombre.Trim(),
            medioId: medioId,
            productTypeId: productTypeId,
            rubroId: rubroId,
            basePrice: basePrice,
            priceDurationDays: priceDurationDays,
            isActive: true,
            fechaCreacion: timeProvider.GetUtcNow().UtcDateTime,
            fechaModificacion: null);
    }

    public Product WithRenamed(string nuevoNombre, TimeProvider timeProvider)
    {
        ValidateNombre(nuevoNombre);
        return new Product(Id, nuevoNombre.Trim(), MedioId, ProductTypeId, RubroId,
            BasePrice, PriceDurationDays, IsActive, FechaCreacion,
            timeProvider.GetUtcNow().UtcDateTime);
    }

    public Product WithUpdatedPrice(decimal basePrice, int? priceDurationDays, TimeProvider timeProvider)
    {
        ValidateBasePrice(basePrice);
        ValidatePriceDurationDays(priceDurationDays);
        return new Product(Id, Nombre, MedioId, ProductTypeId, RubroId,
            basePrice, priceDurationDays, IsActive, FechaCreacion,
            timeProvider.GetUtcNow().UtcDateTime);
    }

    public Product WithUpdatedCategory(int? rubroId, TimeProvider timeProvider)
    {
        ValidateRubroId(rubroId);
        return new Product(Id, Nombre, MedioId, ProductTypeId, rubroId,
            BasePrice, PriceDurationDays, IsActive, FechaCreacion,
            timeProvider.GetUtcNow().UtcDateTime);
    }

    /// <summary>
    /// Combo mutator: renames, updates price and category in one call.
    /// Used by UpdateProductCommandHandler.
    /// </summary>
    public Product WithUpdated(string nombre, int? rubroId, decimal basePrice, int? priceDurationDays, TimeProvider timeProvider)
    {
        ValidateNombre(nombre);
        ValidateRubroId(rubroId);
        ValidateBasePrice(basePrice);
        ValidatePriceDurationDays(priceDurationDays);
        return new Product(Id, nombre.Trim(), MedioId, ProductTypeId, rubroId,
            basePrice, priceDurationDays, IsActive, FechaCreacion,
            timeProvider.GetUtcNow().UtcDateTime);
    }

    public Product WithDeactivated(TimeProvider timeProvider)
        => new(Id, Nombre, MedioId, ProductTypeId, RubroId,
            BasePrice, PriceDurationDays, isActive: false, FechaCreacion,
            timeProvider.GetUtcNow().UtcDateTime);

    // ── Private validators ────────────────────────────────────────────────────

    private static void ValidateNombre(string nombre)
    {
        if (string.IsNullOrWhiteSpace(nombre))
            throw new ArgumentException(
                "El Nombre del producto no puede estar vacío o ser solo espacios.", nameof(nombre));
        if (nombre.Length > NombreMaxLength)
            throw new ArgumentException(
                $"El Nombre del producto no puede superar los {NombreMaxLength} caracteres.", nameof(nombre));
    }

    private static void ValidateMedioId(int medioId)
    {
        if (medioId <= 0)
            throw new ArgumentException("medioId debe ser un entero positivo.", nameof(medioId));
    }

    private static void ValidateProductTypeId(int productTypeId)
    {
        if (productTypeId <= 0)
            throw new ArgumentException("productTypeId debe ser un entero positivo.", nameof(productTypeId));
    }

    private static void ValidateRubroId(int? rubroId)
    {
        if (rubroId.HasValue && rubroId.Value <= 0)
            throw new ArgumentException(
                "rubroId debe ser un entero positivo cuando no es nulo.", nameof(rubroId));
    }

    private static void ValidateBasePrice(decimal basePrice)
    {
        if (basePrice < 0m)
            throw new ArgumentException("basePrice no puede ser negativo.", nameof(basePrice));
    }

    private static void ValidatePriceDurationDays(int? priceDurationDays)
    {
        if (priceDurationDays.HasValue && priceDurationDays.Value <= 0)
            throw new ArgumentException(
                "priceDurationDays debe ser >= 1 cuando se provee.", nameof(priceDurationDays));
    }
}
