﻿using ClassLibrary.Data.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using System.Diagnostics;
using System.Transactions;

namespace ClassLibrary.Data
{
    public class ApplicationDbContext : DbContext
    {
        private readonly ILogger _logger;

        public ApplicationDbContext(
            ILogger<ApplicationDbContext> logger,
            DbContextOptions<ApplicationDbContext> options
        ) : base(options)
        {
            _logger = logger;
            IEnumerableExtentions.Logger = logger;
        }

        public DbSet<AuditHistory> AuditHistory => Set<AuditHistory>();
        public DbSet<Configuration> Configuration => Set<Configuration>();
        public DbSet<Menu> Menu => Set<Menu>();
        public DbSet<Site> Site => Set<Site>();
        public DbSet<Url> Url => Set<Url>();

        protected override void OnModelCreating(ModelBuilder builder)
        {
            base.OnModelCreating(builder);
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
            => optionsBuilder.LogTo(message => _logger.LogDebug("{message}", message));

        public bool HasUnsavedChanges()
        {
            return ChangeTracker.HasChanges();
        }

        public override int SaveChanges(bool acceptAllChangesOnSuccess = true)
        {
            LimitAuditRecords();

            List<AuditEntry> auditEntries = OnBeforeSaveChanges();
            int result = base.SaveChanges(acceptAllChangesOnSuccess);
            OnAfterSaveChanges(auditEntries);

            return result;
        }

        public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
        {
            LimitAuditRecords();

            List<AuditEntry> auditEntries = OnBeforeSaveChanges();
            int result = await base.SaveChangesAsync(true, cancellationToken);
            await OnAfterSaveChangesAsync(auditEntries);

            return result;
        }

        private List<AuditEntry> OnBeforeSaveChanges()
        {
            ChangeTracker.DetectChanges();
            var auditEntries = new List<AuditEntry>();
            foreach (var entry in ChangeTracker.Entries())
            {
                if (entry.Entity is AuditHistory || entry.State == EntityState.Detached || entry.State == EntityState.Unchanged)
                    continue;

                var auditEntry = new AuditEntry(entry)
                {
                    TableName = entry.Metadata.GetTableName() ?? string.Empty,
                    ModifiedOn = DateTime.UtcNow,
                    State = entry.State.ToString()
                };
                auditEntries.Add(auditEntry);

                foreach (var property in entry.Properties)
                {
                    if (property.IsTemporary)
                    {
                        // value will be generated by the database, get the value after saving
                        auditEntry.TemporaryProperties.Add(property);
                        continue;
                    }

                    string propertyName = property.Metadata.Name;
                    if (property.Metadata.IsPrimaryKey())
                    {
                        auditEntry.KeyValues[propertyName] = property.CurrentValue ?? string.Empty;
                        continue;
                    }

                    switch (entry.State)
                    {
                        case EntityState.Added:
                            auditEntry.CurrentValues[propertyName] = property.CurrentValue ?? string.Empty;
                            break;

                        case EntityState.Deleted:
                            auditEntry.OriginalValues[propertyName] = property.OriginalValue ?? string.Empty;
                            break;

                        case EntityState.Modified:
                            if (property.IsModified)
                            {
                                auditEntry.OriginalValues[propertyName] = property.OriginalValue ?? string.Empty;
                                auditEntry.CurrentValues[propertyName] = property.CurrentValue ?? string.Empty;
                            }
                            break;
                    }
                }
            }

            // Save audit entities that have all the modifications
            foreach (var auditEntry in auditEntries.Where(_ => !_.HasTemporaryProperties))
            {
                AuditHistory.Add(auditEntry.ToAuditHistory());
            }

            // keep a list of entries where the value of some properties are unknown at this step
            return auditEntries.Where(_ => _.HasTemporaryProperties).ToList();
        }

        private void OnAfterSaveChanges(List<AuditEntry> auditEntries)
        {
            if (auditEntries == null || auditEntries.Count == 0)
                return;

            foreach (var auditEntry in auditEntries)
            {
                // Get the final value of the temporary properties
                foreach (var prop in auditEntry.TemporaryProperties)
                {
                    if (prop.Metadata.IsPrimaryKey())
                    {
                        auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue ?? string.Empty;
                    }
                    else
                    {
                        auditEntry.CurrentValues[prop.Metadata.Name] = prop.CurrentValue ?? string.Empty;
                    }
                }

                // Save the Audit entry
                AuditHistory.Add(auditEntry.ToAuditHistory());
                SaveChanges();
            }

            return;
        }

        private Task OnAfterSaveChangesAsync(List<AuditEntry> auditEntries)
        {
            if (auditEntries == null || auditEntries.Count == 0)
                return Task.CompletedTask;

            foreach (var auditEntry in auditEntries)
            {
                // Get the final value of the temporary properties
                foreach (var prop in auditEntry.TemporaryProperties)
                {
                    if (prop.Metadata.IsPrimaryKey())
                    {
                        auditEntry.KeyValues[prop.Metadata.Name] = prop.CurrentValue ?? string.Empty;
                    }
                    else
                    {
                        auditEntry.CurrentValues[prop.Metadata.Name] = prop.CurrentValue ?? string.Empty;
                    }
                }

                // Save the Audit entry
                AuditHistory.Add(auditEntry.ToAuditHistory());
            }

            return SaveChangesAsync();
        }

        private void LimitAuditRecords()
        {
            if (this.AuditHistory.Count() > 1000)
                this.AuditHistory.Remove(this.AuditHistory.First());
        }

        // To Initialize: 
        //  dotnet ef migrations add Initial --context ApplicationDbContext --output-dir Migrations
        // To Update:     
        //  dotnet ef migrations add Update --context ApplicationDbContext --output-dir Migrations
        // EF Core tools reference: https://docs.microsoft.com/en-us/ef/core/cli/dotnet
        // Install EF Core Tools: dotnet tool install --global dotnet-ef
        // Upgrade EF Core Tools: dotnet tool update --global dotnet-ef
        // Before you can use the tools on a specific project, you'll need to add the Microsoft.EntityFrameworkCore.Design package to it.
        public static IEnumerable<string> Migrate(IApplicationBuilder app)
        {
            List<string> appliedMigrations = new();

            var scopeFactory = app.ApplicationServices.GetService<IServiceScopeFactory>();
            if (scopeFactory != null)
                using (var serviceScope = scopeFactory.CreateScope())
                {
                    var configurationContext = serviceScope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
                    if (configurationContext != null)
                    {
                        foreach (string migration in configurationContext.Database.GetPendingMigrations())
                            appliedMigrations.Add($"Applied Migration: {migration}");

                        configurationContext.Database.Migrate();

                        if (!configurationContext.Configuration.Any())
                            new Configuration().AddUpdate(configurationContext);
#if DEBUG
                        //TODO: For Development Only
                        if (!configurationContext.Menu.Any())
                        {
                            using (var dbContextTransaction = configurationContext.Database.BeginTransaction())
                            {
                                try
                                {
                                    new Menu
                                    {
                                        Description = "Menu Item 1",
                                        Sites = new List<Site>()
                                        {
                                            new Site { Description = "Web Site 1" },
                                            new Site { Description = "Web Site 2" },
                                            new Site { Description = "Web Site 3" }
                                        }
                                    }.AddUpdate(configurationContext);

                                    new Menu
                                    {
                                        Description = "Menu Item 2",
                                        Sites = new List<Site>()
                                        {
                                            new Site { Description = "Web Site 1" },
                                            new Site { Description = "Web Site 2" },
                                            new Site { Description = "Web Site 3" }
                                        }
                                    }.AddUpdate(configurationContext);

                                    new Menu
                                    {
                                        Description = "Menu Item 3",
                                        Sites = new List<Site>()
                                        {
                                            new Site { Description = "Web Site 1" },
                                            new Site { Description = "Web Site 2" },
                                            new Site { Description = "Web Site 3" }
                                        }
                                    }.AddUpdate(configurationContext);

                                    new Menu
                                    {
                                        Description = "Menu Item 4",
                                        Sites = new List<Site>()
                                        {
                                            new Site { Description = "Web Site 1" },
                                            new Site { Description = "Web Site 2" },
                                            new Site { Description = "Web Site 3" }
                                        }
                                    }.AddUpdate(configurationContext);

                                    new Menu
                                    {
                                        Description = "Menu Item 5",
                                        Sites = new List<Site>()
                                        {
                                            new Site { Description = "Web Site 1" },
                                            new Site { Description = "Web Site 2" },
                                            new Site { Description = "Web Site 3" }
                                        }
                                    }.AddUpdate(configurationContext);

                                    dbContextTransaction.Commit();
                                }
                                catch (Exception)
                                {
                                    dbContextTransaction.Rollback();
                                    throw;
                                }
                            }
                        }
#endif
                    }
                }

            return appliedMigrations;
        }
    }
}
