using System.Linq.Expressions;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.EntityFrameworkCore;

namespace DataModel.Database.Common;

public sealed class DataContext : DbContext, IDataContext
{
    public DbSet<Setting> Settings { get; set; }

    public DbSet<Section> Sections { get; set; }

    public DbSet<TextElement> TextElements { get; set; }

    public DbSet<Translation> Translations { get; set; }

    public DataContext(DbContextOptions<DataContext> contextOptions) : base(contextOptions)
    {
    }
    
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        #region Settings
        
        modelBuilder.Entity<Setting>().HasIndex(n => n.Id);
        modelBuilder.Entity<Setting>().HasIndex(n => n.Code).IsUnique();
        modelBuilder.Entity<Setting>().HasIndex(n => n.BoolValue);
        modelBuilder.Entity<Setting>().HasIndex(n => n.GuidValue);
        modelBuilder.Entity<Setting>().HasIndex(n => n.IntegerValue);
        modelBuilder.Entity<Setting>().HasIndex(n => n.TextValue);

        #endregion

        #region Sections

        modelBuilder.Entity<Section>().HasIndex(n => n.Id);
        modelBuilder.Entity<Section>().HasIndex(n => n.Name);
        modelBuilder.Entity<Section>().HasIndex(n => n.Depth);
        modelBuilder.Entity<Section>().HasIndex(n => n.DataKey);
        // modelBuilder.Entity<Section>().Navigation(n => n.Parent).AutoInclude(); // Cycle-reference, does not work, though.
        modelBuilder.Entity<Section>().Navigation(n => n.TextElements).AutoInclude();

        #endregion

        #region TextElements

        modelBuilder.Entity<TextElement>().HasIndex(n => n.Id);
        modelBuilder.Entity<TextElement>().HasIndex(n => n.Code);
        modelBuilder.Entity<TextElement>().HasIndex(n => n.Name);
        modelBuilder.Entity<TextElement>().HasIndex(n => n.IsMultiLine);
        modelBuilder.Entity<TextElement>().Navigation(n => n.Section).AutoInclude();
        modelBuilder.Entity<TextElement>().Navigation(n => n.Translations).AutoInclude();

        #endregion

        #region Translations

        modelBuilder.Entity<Translation>().HasIndex(n => n.Id);
        modelBuilder.Entity<Translation>().HasIndex(n => n.Culture);
        modelBuilder.Entity<Translation>().HasIndex(n => n.Text);
        modelBuilder.Entity<Translation>().HasIndex(n => n.TranslateManual);
        modelBuilder.Entity<Translation>().Navigation(n => n.TextElement).AutoInclude();

        #endregion
    }

    #region Export and import

    private readonly record struct JsonData(
        IList<JsonSetting> Settings,
        IList<JsonSection> Sections,
        IList<JsonTextElement> TextElements,
        IList<JsonTranslation> Translations
    );

    /// <summary>
    /// Represents a unique identifier for a JSON export and import.
    /// </summary>
    internal readonly record struct JsonUniqueId(string Code, Guid UniqueId, string Prefix = "")
    {
        public override string ToString() => string.IsNullOrWhiteSpace(this.Prefix) ? $"{this.Code}::{this.UniqueId}" : $"{this.Prefix}::{this.Code}::{this.UniqueId}";
        
        public static implicit operator string(JsonUniqueId id) => id.ToString();
    }
    
    /// <summary>
    /// A JSON converter to serialize and deserialize JsonUniqueId instances.
    /// </summary>
    private sealed class JsonUniqueIdConverter : JsonConverter<JsonUniqueId>
    {
        public override JsonUniqueId Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
        {
            var json = reader.GetString();
            var parts = json?.Split("::");
            return parts?.Length switch
            {
                2 => new JsonUniqueId(parts[0], Guid.Parse(parts[1])),
                3 => new JsonUniqueId(parts[1], Guid.Parse(parts[2]), parts[0]),
                
                _ => throw new JsonException($"Invalid format of JsonUniqueId: {json}")
            };
        }

        public override void Write(Utf8JsonWriter writer, JsonUniqueId value, JsonSerializerOptions options)
        {
            writer.WriteStringValue(value);
        }
    }

    internal readonly record struct JsonSetting(
        JsonUniqueId UniqueId,
        string Code,
        string TextValue,
        int IntegerValue,
        bool BoolValue,
        Guid GuidValue
    );
    
    internal readonly record struct JsonSection(
        JsonUniqueId UniqueId,
        string Name,
        string DataKey,
        int Depth,
        JsonUniqueId ParentUniqueId,
        List<JsonUniqueId> TextElements
    );

    internal readonly record struct JsonTextElement(
        JsonUniqueId UniqueId,
        string Code,
        string Name,
        bool IsMultiLine,
        JsonUniqueId SectionUniqueId,
        List<JsonUniqueId> Translations
    );
    
    internal readonly record struct JsonTranslation(
        JsonUniqueId UniqueId,
        string Culture,
        string Text,
        bool TranslateManual,
        JsonUniqueId TextElementUniqueId
    );

    /// <summary>
    /// Exports this database to a JSON file.
    /// </summary>
    /// <param name="path">The path to the JSON file.</param>
    /// <param name="includeSensitiveData">When false, exclude sensitive data from export.</param>
    public async Task ExportAsync(string path, bool includeSensitiveData = false)
    {
        Console.WriteLine("Exporting database to JSON file...");
        var jsonSettings = new JsonSerializerOptions
        {
            WriteIndented = true,
            Converters = { new JsonUniqueIdConverter() },
        };
        
        // Maintained list of sensitive data to be removed from the export:
        var sensitiveSettingCodes = new HashSet<string>
        {
            SettingNames.DEEPL_API_KEY,
        };
        
        // A local filter function to remove sensitive data from the export.
        // Removing just the sensitive values instead of the entire setting.
        IEnumerable<JsonSetting> FilterSensitiveSettings(IEnumerable<JsonSetting> settings)
        {
            foreach (var setting in settings)
            {
                if (sensitiveSettingCodes!.Contains(setting.Code))
                    yield return new JsonSetting(setting.UniqueId, setting.Code, string.Empty, 0, false, Guid.Empty);
                else
                    yield return setting;
            }
        }

        // Use a local reference to the database to use it in the lambda expression trees below:
        // (we cannot use "this" in a lambda expression tree; yields an exception at runtime)
        var db = this;
        
        await using var fileStream = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.None);
        await JsonSerializer.SerializeAsync(fileStream,  
            new JsonData
            {
                // Settings don't have references to other entities; we can just use them here:
                Settings = includeSensitiveData ?
                    // Include all settings, including sensitive data:
                    this.Settings.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSetting()).ToList() :
                    // Exclude sensitive data:
                    FilterSensitiveSettings(this.Settings.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSetting()).AsEnumerable()).ToList(),
                
                // Warning: the parents cannot pre-loaded, thus, we must load them now inside the lambda expression tree:
                Sections = this.Sections.OrderBy(n => n.UniqueId).Select(n => n.ToJsonSection(db)).ToList(),
                
                // All text elements references are pre-loaded, so we can use them here:
                TextElements = this.TextElements.OrderBy(n => n.UniqueId).Select(n => n.ToJsonTextElement()).ToList(),
                
                // All translation references are pre-loaded, so we can use them here:
                Translations = this.Translations.OrderBy(n => n.UniqueId).Select(n => n.ToJsonTranslation()).ToList(),
            }, jsonSettings);
        
        Console.WriteLine("Export complete.");
    }

    /// <summary>
    /// Stores data needed to resolve a parent-child relationship.
    /// </summary>
    /// <param name="ParentId">The parent id we want to resolve.</param>
    /// <param name="Entity">The entity for which we want to resolve the parent.</param>
    /// <typeparam name="T">The type of the entity.</typeparam>
    private readonly record struct TreeResolver<T>(Guid ParentId, T Entity);
    
    /// <summary>
    /// Imports data from a JSON file into an empty database.
    /// </summary>
    /// <param name="path">The path to the JSON export.</param>
    /// <exception cref="InvalidOperationException">When the database is not empty.</exception>
    public async Task ImportAsync(string path)
    {
        if(await this.Settings.AnyAsync() ||
           await this.Sections.AnyAsync() ||
           await this.TextElements.AnyAsync() ||
           await this.Translations.AnyAsync())
            throw new InvalidOperationException("The database is not empty. In order to import data, the database must be empty.");

        Console.WriteLine("Start importing data from JSON file...");
        
        // Start a transaction:
        await using var transaction = await this.Database.BeginTransactionAsync();
        
        // Configure the JSON serializer:
        var jsonSettings = new JsonSerializerOptions
        {
            Converters = { new JsonUniqueIdConverter() },
        };

        await using var fileStream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.Read);
        var jsonData = await JsonSerializer.DeserializeAsync<JsonData>(fileStream, jsonSettings);
        
        // --------------------
        // Import the settings:
        // --------------------
        foreach (var setting in jsonData.Settings)
            this.Settings.Add(Setting.FromJsonSetting(setting));
        
        // --------------------
        // Import the sections:
        // --------------------
        
        // We must store the intermediate data in a list, because we need to resolve
        // the parent-child relationships in a second step.
        var allSections = new Dictionary<Guid, TreeResolver<Section>>();
        var sectionToTextElements = new Dictionary<Guid, List<Guid>>();
        
        // Read the data from the JSON file:
        foreach (var section in jsonData.Sections)
        {
            // Convert the next element:
            var nextSection = Section.FromJsonSection(section);

            // Notice: the parent id is not yet resolved.
            
            // Store the element:
            allSections.Add(nextSection.UniqueId, new (section.ParentUniqueId.UniqueId, nextSection));
            sectionToTextElements.Add(nextSection.UniqueId, section.TextElements.Select(n => n.UniqueId).ToList());
        }
        
        // Now, resolve the parent-child relationships for the sections:
        foreach (var (uniqueId, (parentId, section)) in allSections)
        {
            if(parentId == Guid.Empty)
            {
                if(section.Depth != 0)
                    Console.WriteLine(@$"Section {uniqueId} ""{section.Name}"" has no parent.");
                else
                    Console.WriteLine(@$"Section {uniqueId} ""{section.Name}"" is a root section, thus, has no parent.");
                
                section.Parent = null;
                continue;
            }

            if(allSections.TryGetValue(parentId, out var parent))
                section.Parent = parent.Entity;
            else
            {
                Console.WriteLine(@$"Parent of section {uniqueId} ""{section.Name}"" was not found.");
                section.Parent = null;
                continue;
            }
        }

        // -------------------------
        // Import the text elements:
        // -------------------------
        
        // We must store the intermediate data in a list, because we need to resolve
        // the parent-child relationships in a second step.
        var allTextElements = new Dictionary<Guid, TextElement>();
        var textElementToTranslations = new Dictionary<Guid, List<Guid>>();

        // Read the data from the JSON file:
        foreach (var textElement in jsonData.TextElements)
        {
            // Convert the next element:
            var nextTextElement = TextElement.FromJsonTextElement(textElement);
            
            // We know that the section is already imported, because we imported the sections first:
            nextTextElement.Section = allSections[textElement.SectionUniqueId.UniqueId].Entity;
            
            // Store the element in the list:
            allTextElements.Add(nextTextElement.UniqueId, nextTextElement);
            textElementToTranslations.Add(nextTextElement.UniqueId, textElement.Translations.Select(n => n.UniqueId).ToList());
        }
        
        // Now, resolve the parent-child relationships for the text elements to the sections:
        foreach (var (sectionUniqueId, textElementsIds) in sectionToTextElements)
        {
            var section = allSections[sectionUniqueId].Entity;
            section.TextElements.AddRange(textElementsIds.Select(n => allTextElements[n]));
        }
        
        // Free the memory:
        sectionToTextElements.Clear();

        // ------------------------
        // Import the translations:
        // ------------------------
        
        // We must store the intermediate data in a list, because we need to resolve
        // the parent-child relationships in a second step.
        var allTranslations = new Dictionary<Guid, Translation>();
        
        // Read the data from the JSON file:
        foreach (var translation in jsonData.Translations)
        {
            // Convert the next element:
            var nextTranslation = Translation.FromJsonTranslation(translation);
            
            // We know that the text element is already imported, because we imported the text elements first:
            nextTranslation.TextElement = allTextElements[translation.TextElementUniqueId.UniqueId];
            
            // Store the element in the list:
            allTranslations.Add(nextTranslation.UniqueId, nextTranslation);
        }
        
        // Now, resolve the parent-child relationships for the translations to the text elements:
        foreach (var (textElementUniqueId, translationsIds) in textElementToTranslations)
        {
            var textElement = allTextElements[textElementUniqueId];
            textElement.Translations.AddRange(translationsIds.Select(n => allTranslations[n]));
        }
        
        // Free the memory:
        textElementToTranslations.Clear();
        
        // ---------------------------------
        // Add all the data to the database:
        // ---------------------------------
        this.Sections.AddRange(allSections.Values.Select(n => n.Entity));
        this.TextElements.AddRange(allTextElements.Values);
        this.Translations.AddRange(allTranslations.Values);
        
        // Save the changes:
        await this.SaveChangesAsync();
        
        // Commit the transaction:
        await transaction.CommitAsync();
        
        Console.WriteLine("Finished importing data from JSON file.");
    }

    #endregion

    #region Tools

    internal Task LoadElementsAsync<TEntry, TProp>(TEntry entry, Expression<Func<TEntry, TProp?>> selector) where TEntry : class where TProp : class => this.Entry<TEntry>(entry).Reference(selector).LoadAsync();

    #endregion
}