using Indotalent.ConfigBackEnd.Extensions;
using Indotalent.ConfigBackEnd.Interfaces;
using Indotalent.Data.Abstracts;
using Indotalent.Data.Entities;
using Indotalent.Data.Interfaces;
using Indotalent.Infrastructure.AutoNumberGenerator;
using Indotalent.Shared.Consts;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace Indotalent.Infrastructure.Database;

public class AppDbContext : IdentityDbContext<ApplicationUser>
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ICurrentUserService _currentUserService;

    public AppDbContext(
        DbContextOptions<AppDbContext> options,
        IServiceScopeFactory scopeFactory,
        ICurrentUserService currentUserService) : base(options)
    {
        _scopeFactory = scopeFactory;
        _currentUserService = currentUserService;
    }

    public DbSet<AutoNumberSequence> AutoNumberSequence { get; set; } = default!;
    public DbSet<Currency> Currency { get; set; } = default!;
    public DbSet<Company> Company { get; set; } = default!;
    public DbSet<Tax> Tax { get; set; } = default!;
    public DbSet<PaymentMethod> PaymentMethod { get; set; } = default!;
    public DbSet<Booking> Booking { get; set; } = default!;
    public DbSet<BookingGroup> BookingGroup { get; set; } = default!;
    public DbSet<BookingResource> BookingResource { get; set; } = default!;
    public DbSet<GoodsReceive> GoodsReceive { get; set; } = default!;
    public DbSet<Bill> Bill { get; set; } = default!;
    public DbSet<PaymentDisburse> PaymentDisburse { get; set; } = default!;
    public DbSet<DeliveryOrder> DeliveryOrder { get; set; } = default!;
    public DbSet<Invoice> Invoice { get; set; } = default!;
    public DbSet<PaymentReceive> PaymentReceive { get; set; } = default!;
    public DbSet<DebitNote> DebitNote { get; set; } = default!;
    public DbSet<CreditNote> CreditNote { get; set; } = default!;
    public DbSet<PositiveAdjustment> PositiveAdjustment { get; set; } = default!;
    public DbSet<NegativeAdjustment> NegativeAdjustment { get; set; } = default!;
    public DbSet<ProductGroup> ProductGroup { get; set; } = default!;
    public DbSet<ProgramManager> ProgramManager { get; set; } = default!;
    public DbSet<ProgramManagerResource> ProgramManagerResource { get; set; } = default!;
    public DbSet<PurchaseOrder> PurchaseOrder { get; set; } = default!;
    public DbSet<PurchaseOrderItem> PurchaseOrderItem { get; set; } = default!;
    public DbSet<PurchaseRequisition> PurchaseRequisition { get; set; } = default!;
    public DbSet<PurchaseRequisitionItem> PurchaseRequisitionItem { get; set; } = default!;
    public DbSet<PurchaseReturn> PurchaseReturn { get; set; } = default!;
    public DbSet<Customer> Customer { get; set; } = default!;
    public DbSet<CustomerCategory> CustomerCategory { get; set; } = default!;
    public DbSet<CustomerContact> CustomerContact { get; set; } = default!;
    public DbSet<CustomerGroup> CustomerGroup { get; set; } = default!;
    public DbSet<Product> Product { get; set; } = default!;
    public DbSet<SalesOrder> SalesOrder { get; set; } = default!;
    public DbSet<SalesOrderItem> SalesOrderItem { get; set; } = default!;
    public DbSet<SalesQuotation> SalesQuotation { get; set; } = default!;
    public DbSet<SalesQuotationItem> SalesQuotationItem { get; set; } = default!;
    public DbSet<SalesReturn> SalesReturn { get; set; } = default!;
    public DbSet<Scrapping> Scrapping { get; set; } = default!;
    public DbSet<StockCount> StockCount { get; set; } = default!;
    public DbSet<Todo> Todo { get; set; } = default!;
    public DbSet<TodoItem> TodoItem { get; set; } = default!;
    public DbSet<TransferIn> TransferIn { get; set; } = default!;
    public DbSet<TransferOut> TransferOut { get; set; } = default!;
    public DbSet<UnitMeasure> UnitMeasure { get; set; } = default!;
    public DbSet<Vendor> Vendor { get; set; } = default!;
    public DbSet<VendorCategory> VendorCategory { get; set; } = default!;
    public DbSet<VendorContact> VendorContact { get; set; } = default!;
    public DbSet<VendorGroup> VendorGroup { get; set; } = default!;
    public DbSet<Warehouse> Warehouse { get; set; } = default!;
    public DbSet<InventoryTransaction> InventoryTransaction { get; set; } = default!;

    public override int SaveChanges()
    {
        ApplySoftDelete();
        ApplyAudit(_currentUserService.UserId ?? string.Empty);
        return base.SaveChanges();
    }

    public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
    {
        ChangeTracker.DetectChanges();
        ApplySoftDelete();
        ApplyAudit(_currentUserService.UserId ?? string.Empty);
        await ApplyAutoNumber(cancellationToken);
        return await base.SaveChangesAsync(cancellationToken);
    }

    private void ApplySoftDelete()
    {
        var entries = ChangeTracker.Entries<IHasIsDeleted>()
            .Where(e => e.State == EntityState.Deleted);

        foreach (var entry in entries)
        {
            entry.State = EntityState.Modified;
            entry.Entity.IsDeleted = true;
        }
    }

    private void ApplyAudit(string userId)
    {
        var entries = ChangeTracker.Entries<IHasAudit>()
            .Where(e => e.State == EntityState.Added || e.State == EntityState.Modified)
            .ToList();

        foreach (var entry in entries)
        {
            var now = DateTimeOffset.UtcNow;
            var auditEntity = entry.Entity;

            if (entry.State == EntityState.Added)
            {
                auditEntity.CreatedAt = now;
                auditEntity.CreatedBy = userId;
            }
            else
            {
                entry.Property(nameof(IHasAudit.CreatedAt)).IsModified = false;
                entry.Property(nameof(IHasAudit.CreatedBy)).IsModified = false;
            }

            auditEntity.UpdatedAt = now;
            auditEntity.UpdatedBy = userId;
        }
    }

    private async Task ApplyAutoNumber(CancellationToken ct)
    {
        var entries = ChangeTracker
            .Entries<IHasAutoNumber>()
            .Where(e => e.State == EntityState.Added && string.IsNullOrEmpty(e.Entity.AutoNumber))
            .ToList();

        if (!entries.Any()) return;

        using var scope = _scopeFactory.CreateScope();
        var generator = scope.ServiceProvider.GetRequiredService<AutoNumberGeneratorService>();

        foreach (var entry in entries)
        {
            var entityName = entry.Entity.GetType().Name;
            var shortName = entityName.ToShortNameConsonant(3);

            entry.Entity.AutoNumber = await generator.GenerateNextNumberAsync(
                entityName: entityName,
                prefixTemplate: $"{shortName}/{{Year}}/",
                paddingLength: 4,
                cancellationToken: ct
            );
        }
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        if (Database.ProviderName?.Contains("MySql") == true)
        {
            foreach (var entity in modelBuilder.Model.GetEntityTypes())
            {
                if (entity.GetTableName()!.StartsWith("AspNet"))
                {
                    foreach (var property in entity.GetProperties().Where(p => p.ClrType == typeof(string)))
                    {
                        property.SetMaxLength(191);
                    }
                }
            }
        }

        foreach (var entityType in modelBuilder.Model.GetEntityTypes())
        {
            var type = entityType.ClrType;

            if (typeof(BaseEntity).IsAssignableFrom(type))
            {
                modelBuilder.Entity(type)
                    .Property("Id")
                    .HasMaxLength(GlobalConsts.StringLengthId)
                    .IsFixedLength();
            }

            if (typeof(IHasAudit).IsAssignableFrom(type))
            {
                modelBuilder.Entity(type)
                    .Property(nameof(IHasAudit.CreatedBy))
                    .HasMaxLength(GlobalConsts.StringLengthShort);

                modelBuilder.Entity(type)
                    .Property(nameof(IHasAudit.UpdatedBy))
                    .HasMaxLength(GlobalConsts.StringLengthShort);
            }

            if (typeof(IHasIsDeleted).IsAssignableFrom(type))
            {
                modelBuilder.Entity(type).HasQueryFilter(GetNotDeletedOnlyFilter(type));
            }
        }

        modelBuilder.ApplyConfigurationsFromAssembly(typeof(AppDbContext).Assembly);
    }

    private static System.Linq.Expressions.LambdaExpression GetNotDeletedOnlyFilter(Type type)
    {
        var parameter = System.Linq.Expressions.Expression.Parameter(type, "e");
        var property = System.Linq.Expressions.Expression.Property(parameter, nameof(IHasIsDeleted.IsDeleted));
        var falseConstant = System.Linq.Expressions.Expression.Constant(false);
        var comparison = System.Linq.Expressions.Expression.Equal(property, falseConstant);
        return System.Linq.Expressions.Expression.Lambda(comparison, parameter);
    }
}