namespace DocuMed.Repository.Extensions;

public class ModelBuilderQueryFilter
{
    public void AddQueryFilterToModelBuilder(ModelBuilder modelBuilder, Type type)
    {
        var method = GetType().GetMethod("RegisterQueryFilter").MakeGenericMethod(type);
        method.Invoke(this, new object[] { modelBuilder });
    }

    public void RegisterQueryFilter<TQFilter>(ModelBuilder modelBuilder) where TQFilter : ApiEntity
    {
        var tt = typeof(TQFilter);
        if (tt.BaseType == typeof(ApiEntity))
            modelBuilder.Entity<TQFilter>().HasQueryFilter(e => e.IsRemoved == false);
    }
}

public static class ModelBuilderExtensions
{
    /// <summary>
    ///     Singularizin table name like Posts to Post or People to Person
    /// </summary>
    /// <param name="modelBuilder"></param>
    public static void AddSingularizingTableNameConvention(this ModelBuilder modelBuilder)
    {
        var pluralizer = new Pluralizer();
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            var tableName = entityType.GetTableName();
            entityType.SetTableName(pluralizer.Singularize(tableName));
        }
    }


    /// <summary>
    ///     Set NEWSEQUENTIALID() sql function for all columns named "Id"
    /// </summary>
    /// <param name="modelBuilder"></param>
    /// <param name="mustBeIdentity">Set to true if you want only "Identity" guid fields that named "Id"</param>
    public static void AddSequentialGuidForIdConvention(this ModelBuilder modelBuilder)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            var property = entityType.GetProperties()
                .Where(p => p.Name.Contains("Id", StringComparison.OrdinalIgnoreCase))
                .ToArray();
            foreach (var mutableProperty in property)
                modelBuilder.AddDefaultValueSqlConvention(mutableProperty?.Name, typeof(Guid), "gen_random_uuid()");
        }
    }

    /// <summary>
    ///     Set DefaultValueSql for sepecific property name and type
    /// </summary>
    /// <param name="modelBuilder"></param>
    /// <param name="propertyName">Name of property wants to set DefaultValueSql for</param>
    /// <param name="propertyType">Type of property wants to set DefaultValueSql for </param>
    /// <param name="defaultValueSql">DefaultValueSql like "NEWSEQUENTIALID()"</param>
    public static void AddDefaultValueSqlConvention(this ModelBuilder modelBuilder, string propertyName,
        Type propertyType, string defaultValueSql)
    {
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            var property = entityType.GetProperties()
                .SingleOrDefault(p => p.Name.Equals(propertyName, StringComparison.OrdinalIgnoreCase));
            if (property != null && property.ClrType == propertyType)
                property.SetDefaultValueSql(defaultValueSql);
        }
    }

    /// <summary>
    ///     Set DeleteBehavior.Restrict by default for relations
    /// </summary>
    /// <param name="modelBuilder"></param>
    public static void AddRestrictDeleteBehaviorConvention(this ModelBuilder modelBuilder)
    {
        var cascadeFKs = modelBuilder.Model.GetEntityTypes()
            .SelectMany(t => t.GetForeignKeys())
            .Where(fk => !fk.IsOwnership && fk.DeleteBehavior == DeleteBehavior.Cascade);
        foreach (var fk in cascadeFKs)
            fk.DeleteBehavior = DeleteBehavior.Restrict;
    }

    /// <summary>
    ///     Dynamicaly load all IEntityTypeConfiguration with Reflection
    /// </summary>
    /// <param name="modelBuilder"></param>
    /// <param name="assemblies">Assemblies contains Entities</param>
    public static void RegisterEntityTypeConfiguration(this ModelBuilder modelBuilder, params Assembly[] assemblies)
    {
        var applyGenericMethod = typeof(ModelBuilder)
            .GetMethods()
            .First(m => m.Name == nameof(ModelBuilder.ApplyConfiguration));

        var types = assemblies.SelectMany(a => a.GetExportedTypes())
            .Where(c => c.IsClass && !c.IsAbstract && c.IsPublic);

        foreach (var type in types)
        foreach (var iface in type.GetInterfaces())
            if (iface.IsConstructedGenericType &&
                iface.GetGenericTypeDefinition() == typeof(IEntityTypeConfiguration<>))
            {
                var applyConcreteMethod = applyGenericMethod.MakeGenericMethod(iface.GenericTypeArguments[0]);
                applyConcreteMethod.Invoke(modelBuilder, new[] { Activator.CreateInstance(type) });
            }
    }


    /// <summary>
    ///     Pluralizing table name like Post to Posts or Person to People
    /// </summary>
    /// <param name="modelBuilder"></param>
    public static void AddPluralizingTableNameConvention(this ModelBuilder modelBuilder)
    {
        var pluralizer = new Pluralizer();
        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
            if (entityType.BaseType == null)
            {
                var tableName = entityType.GetTableName();
                entityType.SetTableName(pluralizer.Pluralize(tableName));
            }
    }

    /// <summary>
    ///     Dynamicaly register all Entities that inherit from specific BaseType
    /// </summary>
    /// <param name="modelBuilder"></param>
    /// <param name="baseType">Base type that Entities inherit from this</param>
    /// <param name="assemblies">Assemblies contains Entities</param>
    public static void RegisterAllEntities<BaseType>(this ModelBuilder modelBuilder,
        ILogger<ApplicationContext> _logger, params Assembly[] assemblies) where BaseType : ApiEntity
    {
        var types = assemblies.SelectMany(a => a.GetExportedTypes())
            .Where(c => c.IsClass && !c.IsAbstract && c.IsPublic && typeof(BaseType)
                .IsAssignableFrom(c));
        var builderQueryFilter = new ModelBuilderQueryFilter();
        foreach (var type in types)
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            modelBuilder.Entity(type);
            builderQueryFilter.AddQueryFilterToModelBuilder(modelBuilder, type);
            stopwatch.Stop();
        }
    }

    /// <summary>
    ///     Dynamicaly register all Entities that inherit from specific BaseType
    /// </summary>
    /// <param name="modelBuilder"></param>
    /// <param name="baseType">Base type that Entities inherit from this</param>
    /// <param name="assemblies">Assemblies contains Entities</param>
    public static void RegisterAllEntitiesV02<BaseType>(this ModelBuilder builder, ILogger<ApplicationContext> _logger, params Assembly[] assemblies) where BaseType : ApiEntity
    {
        var types = assemblies.SelectMany(a => a.GetExportedTypes())
            .Where(c => c.IsClass && !c.IsAbstract && c.IsPublic && typeof(BaseType)
                .IsAssignableFrom(c));
        var builderQueryFilter = new ModelBuilderQueryFilter();

        foreach (var type in types)
        {
            var stopwatch = new Stopwatch();
            stopwatch.Start();
            // On Model Creating
            var onModelCreatingMethod =
                type.GetMethods().FirstOrDefault(x => x.Name == "OnModelCreating");

            if (onModelCreatingMethod != null) 
                onModelCreatingMethod.Invoke(type, new object[] { builder });
            else
            {
                // On Base Model Creating
                if (type.BaseType == null || type.BaseType != typeof(BaseType)) continue;

                var baseOnModelCreatingMethod = type.BaseType.GetMethods()
                    .FirstOrDefault(x => x.Name == "OnModelCreating");

                if (baseOnModelCreatingMethod == null) 
                    continue;

                baseOnModelCreatingMethod.Invoke(typeof(BaseType), new object[] { builder });
            }

            builderQueryFilter.AddQueryFilterToModelBuilder(builder, type);
            stopwatch.Stop();
            _logger.LogInformation($"MODEL BUILDER {type.Name} In : {stopwatch.ElapsedMilliseconds}ms");
        }
    }
}