using System.IO.Compression;
using System.IO;
using System.Threading.Tasks;
using Elwig.Models.Entities;
using System.Collections.Generic;
using System;
using System.Text.Json.Nodes;
using System.Linq;
using System.Windows;
using Microsoft.EntityFrameworkCore;
using System.Text.Json;

namespace Elwig.Helpers.Export {
    public static class ElwigData {

        public enum ImportMode { Auto, Interactively, FromBranches }

        public static readonly string ImportedTxt = Path.Combine(App.DataPath, "imported.txt");

        private static readonly JsonSerializerOptions JsonOpts = new() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

        public static async Task<string[]> GetImportedFiles() {
            try {
                return await File.ReadAllLinesAsync(ImportedTxt, Utils.UTF8);
            } catch {
                return [];
            }
        }

        public static async Task AddImportedFiles(params string[] filenames) {
            await File.AppendAllLinesAsync(ImportedTxt, filenames, Utils.UTF8);
        }

        public static Task Import(string filename, ImportMode mode) => Import([filename], mode);

        public static async Task Import(IEnumerable<string> filenames, ImportMode mode) {
            try {
                Dictionary<string, Branch> branches;
                Dictionary<int, int> currentDids;
                Dictionary<string, int> currentLsNrs;
                Dictionary<int, List<WbRd>> currentWbRde;
                Dictionary<int, AT_Kg> kgs;

                using (var ctx = new AppDbContext()) {
                    branches = await ctx.Branches.ToDictionaryAsync(b => b.ZwstId);
                    currentDids = await ctx.Deliveries
                        .GroupBy(d => d.Year)
                        .ToDictionaryAsync(g => g.Key, g => g.Max(d => d.DId));
                    currentLsNrs = await ctx.Deliveries
                        .ToDictionaryAsync(d => d.LsNr, d => d.DId);
                    currentWbRde = await ctx.WbRde
                        .GroupBy(r => r.KgNr)
                        .ToDictionaryAsync(g => g.Key, g => g.ToList());
                    kgs = await ctx.Katastralgemeinden.Include(k => k.WbKg).ToDictionaryAsync(k => k.KgNr);
                }

                var data = new List<(
                    List<Member> Members,
                    List<BillingAddr> BillingAddresses,
                    List<MemberTelNr> TelephoneNumbers,
                    List<MemberEmailAddr> EmailAddresses,
                    List<AreaCom> AreaCommitments,
                    List<WbRd> Riede,
                    List<Delivery> Deliveries,
                    List<DeliveryPart> DeliveryParts,
                    List<DeliveryPartModifier> Modifiers)>();

                var metaData = new List<(string FileName, string ZwstId, string Device,
                    int? MemberNum, string? MemberFilters,
                    int? AreaComNum, string? AreaComFilters,
                    int? DeliveryNum, string? DeliveryFilters)>();

                foreach (var filename in filenames) {
                    // TODO read encrypted files
                    using var zip = ZipFile.Open(filename, ZipArchiveMode.Read);

                    var version = zip.GetEntry("version");
                    using (var reader = new StreamReader(version!.Open(), Utils.UTF8)) {
                        if (await reader.ReadToEndAsync() != "elwig:1")
                            throw new FileFormatException($"Ungültige Export-Datei ({filename})");
                    }

                    var metaJson = zip.GetEntry("meta.json");
                    var meta = await JsonNode.ParseAsync(metaJson!.Open());
                    var memberCount = meta!["members"]?["count"]?.AsValue().GetValue<int>();
                    var memberFilters = meta!["members"]?["filters"]?.AsArray().Select(f => f!.AsValue().GetValue<string>()).ToArray();
                    var areaComCount = meta!["area_commitments"]?["count"]?.AsValue().GetValue<int>();
                    var areaComFilters = meta!["area_commitments"]?["filters"]?.AsArray().Select(f => f!.AsValue().GetValue<string>()).ToArray();
                    var deliveryCount = meta!["deliveries"]?["count"]?.AsValue().GetValue<int>();
                    var deliveryFilters = meta!["deliveries"]?["filters"]?.AsArray().Select(f => f!.AsValue().GetValue<string>()).ToArray();
                    metaData.Add((Path.GetFileName(filename),
                        meta["zwstid"]!.AsValue().GetValue<string>(), meta["device"]!.AsValue().GetValue<string>(),
                        memberCount, memberFilters != null ? string.Join(" / ", memberFilters) : null,
                        areaComCount, areaComFilters != null ? string.Join(" / ", areaComFilters) : null,
                        deliveryCount, deliveryFilters != null ? string.Join(" / ", deliveryFilters) : null));

                    data.Add(new([], [], [], [], [], [], [], new([], [])));
                    var r = data[^1];

                    var membersJson = zip.GetEntry("members.json");
                    if (membersJson != null) {
                        using var reader = new StreamReader(membersJson.Open(), Utils.UTF8);
                        string? line;
                        while ((line = await reader.ReadLineAsync()) != null) {
                            var obj = JsonNode.Parse(line)!.AsObject();
                            var (m, b, telNrs, emailAddrs) = obj.ToMember(kgs);
                            r.Members.Add(m);
                            if (b != null) r.BillingAddresses.Add(b);
                            r.TelephoneNumbers.AddRange(telNrs);
                            r.EmailAddresses.AddRange(emailAddrs);
                        }
                    }

                    var areaComsJson = zip.GetEntry("area_commitments.json");
                    if (areaComsJson != null) {
                        using var reader = new StreamReader(areaComsJson.Open(), Utils.UTF8);
                        string? line;
                        while ((line = await reader.ReadLineAsync()) != null) {
                            var obj = JsonNode.Parse(line)!.AsObject();
                            var (areaCom, wbrd) = obj.ToAreaCom(kgs, currentWbRde);
                            r.AreaCommitments.Add(areaCom);
                            if (wbrd != null) {
                                currentWbRde[wbrd.KgNr].Add(wbrd);
                                r.Riede.Add(wbrd);
                            }
                        }
                    }

                    var deliveriesJson = zip.GetEntry("deliveries.json");
                    if (deliveriesJson != null) {
                        using var reader = new StreamReader(deliveriesJson.Open(), Utils.UTF8);
                        string? line;
                        while ((line = await reader.ReadLineAsync()) != null) {
                            var obj = JsonNode.Parse(line)!.AsObject();
                            var (d, parts, mods) = obj.ToDelivery(currentLsNrs, currentDids);
                            r.Deliveries.Add(d);
                            r.DeliveryParts.AddRange(parts);
                            r.Modifiers.AddRange(mods);
                        }
                    }
                }

                var importedMembers = new List<(string FileName, string ZwstId, string Device, int New, int Overwritten, int NotImported, string Filters)>();
                var importedAreaComs = new List<(string FileName, string ZwstId, string Device, int Imported, int NotImported, string Filters)>();
                var importedDeliveries = new List<(string FileName, string ZwstId, string Device, int New, int Overwritten, int NotImported, string Filters)>();

                foreach (var ((members, billingAddresses, telephoneNumbers, emailAddresses, areaCommitments, riede, deliveries, deliveryParts, modifiers), meta) in data.Zip(metaData)) {
                    var branch = branches[meta.ZwstId];
                    var device = meta.Device;

                    using var ctx = new AppDbContext();

                    var mgnrs = members.Select(m => m.MgNr).ToList();
                    var duplicateMgNrs = await ctx.Members
                        .Where(m => mgnrs.Contains(m.MgNr))
                        .Select(m => m.MgNr)
                        .ToListAsync();
                    bool importNewMembers = false, importDuplicateMembers = false;
                    if (mode == ImportMode.Interactively) {
                        if (mgnrs.Count - duplicateMgNrs.Count > 0)
                            importNewMembers = ImportQuestion(branch.Name, device, "Mitglieder", false, mgnrs.Count - duplicateMgNrs.Count);
                    } else {
                        importNewMembers = true;
                    }
                    if (duplicateMgNrs.Count > 0)
                        importDuplicateMembers = ImportQuestion(branch.Name, device, "Mitglieder", true, duplicateMgNrs.Count);

                    var fbnrs = areaCommitments.Select(c => c.FbNr).ToList();
                    var duplicateFbNrs = await ctx.AreaCommitments
                        .Where(c => fbnrs.Contains(c.FbNr))
                        .Select(c => c.FbNr)
                        .ToListAsync();

                    var lsnrs = deliveries.Select(d => d.LsNr).ToList();
                    var duplicateLsNrs = await ctx.Deliveries
                        .Where(d => lsnrs.Contains(d.LsNr))
                        .Select(d => d.LsNr)
                        .ToListAsync();
                    var duplicateDIds = deliveries
                        .Where(d => duplicateLsNrs.Contains(d.LsNr))
                        .Select(d => (d.Year, d.DId))
                        .ToList();
                    var allowedDuplicateLsNrs = new List<string>();
                    bool importNewDeliveries = false, importDuplicateDeliveries = false;
                    if (mode == ImportMode.Interactively) {
                        if (lsnrs.Count - duplicateLsNrs.Count > 0)
                            importNewDeliveries = ImportQuestion(branch.Name, device, "Lieferungen", false, lsnrs.Count - duplicateLsNrs.Count);
                        if (duplicateLsNrs.Count > 0)
                            importDuplicateDeliveries = ImportQuestion(branch.Name, device, "Lieferungen", true, duplicateLsNrs.Count);
                    } else if (mode == ImportMode.FromBranches) {
                        importNewDeliveries = true;
                        if (duplicateLsNrs.Count > 0) {
                            allowedDuplicateLsNrs = await ctx.Deliveries
                                .Where(d => lsnrs.Contains(d.LsNr) && d.ZwstId == branch.ZwstId)
                                .Select(d => d.LsNr)
                                .ToListAsync();
                            if (duplicateLsNrs.Count - allowedDuplicateLsNrs.Count > 0)
                                importDuplicateDeliveries = ImportQuestion(branch.Name, device, "Lieferungen", true, duplicateLsNrs.Count - allowedDuplicateLsNrs.Count);
                        }
                    } else {
                        importNewDeliveries = true;
                        if (duplicateLsNrs.Count > 0)
                            importDuplicateDeliveries = ImportQuestion(branch.Name, device, "Lieferungen", true, duplicateLsNrs.Count);
                    }

                    if (importDuplicateMembers) {
                        ctx.RemoveRange(ctx.BillingAddresses.Where(a => duplicateMgNrs.Contains(a.MgNr)));
                        ctx.RemoveRange(ctx.MemberTelephoneNrs.Where(n => duplicateMgNrs.Contains(n.MgNr)));
                        ctx.RemoveRange(ctx.MemberEmailAddrs.Where(a => duplicateMgNrs.Contains(a.MgNr)));
                        ctx.UpdateRange(members.Where(m => duplicateMgNrs.Contains(m.MgNr)));
                        ctx.AddRange(billingAddresses.Where(a => duplicateMgNrs.Contains(a.MgNr)));
                        ctx.AddRange(telephoneNumbers.Where(n => duplicateMgNrs.Contains(n.MgNr)));
                        ctx.AddRange(emailAddresses.Where(a => duplicateMgNrs.Contains(a.MgNr)));
                        ctx.UpdateRange(areaCommitments.Where(c => duplicateMgNrs.Contains(c.MgNr) && duplicateFbNrs.Contains(c.FbNr)));
                        ctx.AddRange(areaCommitments.Where(c => duplicateMgNrs.Contains(c.MgNr) && !duplicateFbNrs.Contains(c.FbNr)));
                    }
                    if (importNewMembers) {
                        ctx.AddRange(members.Where(m => !duplicateMgNrs.Contains(m.MgNr)));
                        ctx.AddRange(billingAddresses.Where(a => !duplicateMgNrs.Contains(a.MgNr)));
                        ctx.AddRange(telephoneNumbers.Where(n => !duplicateMgNrs.Contains(n.MgNr)));
                        ctx.AddRange(emailAddresses.Where(a => !duplicateMgNrs.Contains(a.MgNr)));
                        ctx.UpdateRange(areaCommitments.Where(c => !duplicateMgNrs.Contains(c.MgNr) && duplicateFbNrs.Contains(c.FbNr)));
                        ctx.AddRange(areaCommitments.Where(c => !duplicateMgNrs.Contains(c.MgNr) && !duplicateFbNrs.Contains(c.FbNr)));
                    }
                    if (members.Count > 0) {
                        var n = importNewMembers ? members.Count - duplicateMgNrs.Count : 0;
                        var o = importDuplicateMembers ? duplicateMgNrs.Count : 0;
                        importedMembers.Add((meta.FileName, meta.ZwstId, meta.Device, n, o, members.Count - n - o, meta.MemberFilters));
                    }
                    if (areaCommitments.Count > 0) {
                        ctx.AddRange(riede);
                        var imported = areaCommitments.Where(c => (importNewMembers && !duplicateMgNrs.Contains(c.MgNr)) || (importDuplicateMembers && duplicateMgNrs.Contains(c.MgNr))).ToList();
                        importedAreaComs.Add((meta.FileName, meta.ZwstId, meta.Device, imported.Count, areaCommitments.Count - imported.Count, meta.AreaComFilters));
                    }

                    if (allowedDuplicateLsNrs.Count > 0) {
                        var dids = deliveries
                            .Where(d => allowedDuplicateLsNrs.Contains(d.LsNr))
                            .Select(d => (d.Year, d.DId))
                            .ToList();
                        ctx.RemoveRange(ctx.DeliveryParts
                            .Where(p => allowedDuplicateLsNrs.Contains(p.Delivery.LsNr))
                            .SelectMany(p => p.PartModifiers));
                        ctx.RemoveRange(ctx.DeliveryParts.Where(p => allowedDuplicateLsNrs.Contains(p.Delivery.LsNr)));
                        ctx.UpdateRange(deliveries.Where(d => dids.Contains((d.Year, d.DId))));
                        ctx.AddRange(deliveryParts.Where(p => dids.Contains((p.Year, p.DId))));
                        ctx.AddRange(modifiers.Where(m => dids.Contains((m.Year, m.DId))));
                    }
                    if (importDuplicateDeliveries) {
                        var l = duplicateLsNrs.Except(allowedDuplicateLsNrs).ToList();
                        var dids = deliveries
                            .Where(d => l.Contains(d.LsNr))
                            .Select(d => (d.Year, d.DId))
                            .ToList();
                        ctx.RemoveRange(ctx.DeliveryParts
                            .Where(p => l.Contains(p.Delivery.LsNr))
                            .SelectMany(p => p.PartModifiers));
                        ctx.RemoveRange(ctx.DeliveryParts.Where(p => l.Contains(p.Delivery.LsNr)));
                        ctx.UpdateRange(deliveries.Where(d => dids.Contains((d.Year, d.DId))));
                        ctx.AddRange(deliveryParts.Where(p => dids.Contains((p.Year, p.DId))));
                        ctx.AddRange(modifiers.Where(m => dids.Contains((m.Year, m.DId))));
                    }
                    if (importNewDeliveries) {
                        ctx.AddRange(deliveries.Where(d => !duplicateDIds.Contains((d.Year, d.DId))));
                        ctx.AddRange(deliveryParts.Where(p => !duplicateDIds.Contains((p.Year, p.DId))));
                        ctx.AddRange(modifiers.Where(m => !duplicateDIds.Contains((m.Year, m.DId))));
                    }
                    if (deliveries.Count > 0) {
                        var n = importNewDeliveries ? deliveries.Count - duplicateDIds.Count : 0;
                        var o = allowedDuplicateLsNrs.Count + (importDuplicateDeliveries ? duplicateDIds.Count - allowedDuplicateLsNrs.Count : 0);
                        importedDeliveries.Add((meta.FileName, meta.ZwstId, meta.Device, n, o, deliveries.Count - n - o, meta.DeliveryFilters));
                    }

                    await ctx.SaveChangesAsync();
                    await AddImportedFiles(Path.GetFileName(meta.FileName));
                }
                App.HintContextChange();

                MessageBox.Show(
                    $"Das importieren der Daten war erfolgreich!\n" +
                    $"Folgendes wurde importiert:\n" +
                    string.Join("\n", [
                        $"Mitglieder: {importedMembers.Sum(d => d.New + d.Overwritten)}",
                        ..importedMembers.Select(d =>
                            $"  {d.FileName} ({d.New + d.Overwritten})\n" +
                            $"    ({d.New} neu, {d.Overwritten} überschrieben, {d.NotImported} nicht importiert)\n" +
                            $"    Zweigstelle: {branches[d.ZwstId].Name} (Gerät {d.Device})\n" +
                            $"    Filter: {d.Filters}"),
                        $"Flächenbindungen: {importedAreaComs.Sum(d => d.Imported)}",
                        ..importedAreaComs.Select(d =>
                            $"  {d.FileName} ({d.Imported})\n" +
                            $"    ({d.Imported} importiert, {d.NotImported} nicht importiert)\n" +
                            $"    Zweigstelle: {branches[d.ZwstId].Name} (Gerät {d.Device})\n" +
                            $"    Filter: {d.Filters}"),
                        $"Lieferungen: {importedDeliveries.Sum(d => d.New + d.Overwritten)}",
                        ..importedDeliveries.Select(d =>
                            $"  {d.FileName} ({d.New + d.Overwritten})\n" +
                            $"    ({d.New} neu, {d.Overwritten} überschr., {d.NotImported} nicht importiert)\n" +
                            $"    Zwst.: {branches[d.ZwstId].Name} (Gerät {d.Device})\n" +
                            $"    Filter: {d.Filters}")
                    ]),
                    "Importieren erfolgreich",
                    MessageBoxButton.OK, MessageBoxImage.Information);
            } catch (Exception exc) {
                var str = "Der Eintrag konnte nicht in der Datenbank aktualisiert werden!\n\n" + exc.Message;
                if (exc.InnerException != null) str += "\n\n" + exc.InnerException.Message;
                MessageBox.Show(str, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
            }
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }

        private static bool ImportQuestion(string branch, string device, string subject, bool duplicate, int number) {
            return MessageBox.Show(
                $"Sollen {number} {(duplicate ? "" : "neue ")}{subject} durch die Zweigstelle\n" +
                $"{branch} (Gerät {device}) {(duplicate ? "überschrieben" : "importiert")} werden?",
                $"{subject} importieren",
                MessageBoxButton.YesNo, MessageBoxImage.Question, MessageBoxResult.Yes
            ) == MessageBoxResult.Yes;
        }

        public static Task Export(string filename, IEnumerable<Member> members, IEnumerable<string> filters) {
            return new ElwigExport {
                Members = (members, filters)
            }.Export(filename);
        }

        public static Task Export(string filename, IEnumerable<Member> members, IEnumerable<AreaCom> areaComs, IEnumerable<string> filters) {
            return new ElwigExport {
                Members = (members, filters),
                AreaComs = (areaComs, ["von exportierten Mitgliedern"]),
            }.Export(filename);
        }

        public static Task Export(string filename, IEnumerable<Delivery> deliveries, IEnumerable<string> filters) {
            return new ElwigExport {
                Deliveries = (deliveries, filters)
            }.Export(filename);
        }

        public class ElwigExport {
            public (IEnumerable<Member> Members, IEnumerable<string> Filters)? Members { get; set; }
            public (IEnumerable<AreaCom> AreaComs, IEnumerable<string> Filters)? AreaComs { get; set; }
            public (IEnumerable<Delivery> Deliveries, IEnumerable<string> Filters)? Deliveries { get; set; }

            public async Task Export(string filename) {
                File.Delete(filename);
                using var zip = ZipFile.Open(filename, ZipArchiveMode.Create);

                var version = zip.CreateEntry("version", CompressionLevel.NoCompression);
                using (var writer = new StreamWriter(version.Open(), Utils.UTF8)) {
                    await writer.WriteAsync("elwig:1");
                }

                var meta = zip.CreateEntry("meta.json", CompressionLevel.NoCompression);
                using (var writer = new StreamWriter(meta.Open(), Utils.UTF8)) {
                    var obj = new JsonObject {
                        ["timestamp"] = $"{DateTime.UtcNow:yyyy-MM-ddTHH:mm:ssZ}",
                        ["zwstid"] = App.ZwstId,
                        ["device"] = Environment.MachineName,
                    };
                    if (Members != null)
                        obj["members"] = new JsonObject {
                            ["count"] = Members.Value.Members.Count(),
                            ["filters"] = new JsonArray(Members.Value.Filters.Select(f => (JsonNode)f).ToArray()),
                        };
                    if (AreaComs != null)
                        obj["area_commitments"] = new JsonObject {
                            ["count"] = AreaComs.Value.AreaComs.Count(),
                            ["filters"] = new JsonArray(AreaComs.Value.Filters.Select(f => (JsonNode)f).ToArray()),
                        };
                    if (Deliveries != null)
                        obj["deliveries"] = new JsonObject {
                            ["count"] = Deliveries.Value.Deliveries.Count(),
                            ["parts"] = Deliveries.Value.Deliveries.Sum(d => d.Parts.Count),
                            ["filters"] = new JsonArray(Deliveries.Value.Filters.Select(f => (JsonNode)f).ToArray()),
                        };
                    await writer.WriteAsync(obj.ToJsonString(JsonOpts));
                }

                // TODO encrypt files
                if (Members != null) {
                    var json = zip.CreateEntry("members.json", CompressionLevel.SmallestSize);
                    using var writer = new StreamWriter(json.Open(), Utils.UTF8);
                    foreach (var m in Members.Value.Members) {
                        await writer.WriteLineAsync(m.ToJson().ToJsonString(JsonOpts));
                    }
                }
                if (AreaComs != null) {
                    var json = zip.CreateEntry("area_commitments.json", CompressionLevel.SmallestSize);
                    using var writer = new StreamWriter(json.Open(), Utils.UTF8);
                    foreach (var c in AreaComs.Value.AreaComs) {
                        await writer.WriteLineAsync(c.ToJson().ToJsonString(JsonOpts));
                    }
                }
                if (Deliveries != null) {
                    var json = zip.CreateEntry("deliveries.json", CompressionLevel.SmallestSize);
                    using var writer = new StreamWriter(json.Open(), Utils.UTF8);
                    foreach (var d in Deliveries.Value.Deliveries) {
                        await writer.WriteLineAsync(d.ToJson().ToJsonString(JsonOpts));
                    }
                }
            }
        }

        public static JsonObject ToJson(this Member m) {
            return new JsonObject {
                ["mgnr"] = m.MgNr,
                ["predecessor_mgnr"] = m.PredecessorMgNr,
                ["name"] = m.Name,
                ["prefix"] = m.Prefix,
                ["given_name"] = m.GivenName,
                ["middle_names"] = m.MiddleName,
                ["suffix"] = m.Suffix,
                ["attn"] = m.ForTheAttentionOf,
                ["birthday"] = m.Birthday,
                ["entry_date"] = m.EntryDate != null ? $"{m.EntryDate:yyyy-MM-dd}" : null,
                ["exit_date"] = m.ExitDate != null ? $"{m.ExitDate:yyyy-MM-dd}" : null,
                ["business_shares"] = m.BusinessShares,
                ["accounting_nr"] = m.AccountingNr,
                ["zwstid"] = m.ZwstId,
                ["lfbis_nr"] = m.LfbisNr,
                ["ustid_nr"] = m.UstIdNr,
                ["juridical_pers"] = m.IsJuridicalPerson,
                ["volllieferant"] = m.IsVollLieferant,
                ["buchführend"] = m.IsBuchführend,
                ["organic"] = m.IsOrganic,
                ["funktionär"] = m.IsFunktionär,
                ["active"] = m.IsActive,
                ["deceased"] = m.IsDeceased,
                ["iban"] = m.Iban,
                ["bic"] = m.Bic,
                ["default_kgnr"] = m.DefaultKgNr,
                ["contact_postal"] = m.ContactViaPost,
                ["contact_email"] = m.ContactViaEmail,
                ["address"] = new JsonObject {
                    ["address"] = m.Address,
                    ["postal_dest"] = m.PostalDestId,
                    ["country"] = m.CountryNum,
                },
                ["billing_address"] = m.BillingAddress != null ? new JsonObject {
                    ["name"] = m.BillingAddress.FullName,
                    ["address"] = m.BillingAddress.Address,
                    ["postal_dest"] = m.BillingAddress.PostalDestId,
                    ["country"] = m.BillingAddress.CountryNum,
                } : null,
                ["telephone_numbers"] = new JsonArray(m.TelephoneNumbers.OrderBy(n => n.Nr).Select(n => {
                    var obj = new JsonObject {
                        ["number"] = n.Number,
                        ["type"] = n.Type,
                    };
                    if (n.Comment != null) obj["comment"] = n.Comment;
                    return obj;
                }).ToArray()),
                ["email_addresses"] = new JsonArray(m.EmailAddresses.OrderBy(a => a.Nr).Select(a => {
                    var obj = new JsonObject {
                        ["address"] = a.Address,
                    };
                    if (a.Comment != null) obj["comment"] = a.Comment;
                    return obj;
                }).ToArray()),
                ["comment"] = m.Comment,
            };
        }

        public static (Member, BillingAddr?, List<MemberTelNr>, List<MemberEmailAddr>) ToMember(this JsonNode json, Dictionary<int, AT_Kg> kgs) {
            var mgnr = json["mgnr"]!.AsValue().GetValue<int>();
            var kgnr = json["default_kgnr"]?.AsValue().GetValue<int>();
            if (kgnr != null && !kgs.Values.Any(k => k.WbKg?.KgNr == kgnr)) {
                throw new ArgumentException($"Für KG {(kgs.TryGetValue(kgnr.Value, out var k) ? k.Name : "?")} ({kgnr:00000}) ist noch keine Großlage festgelegt!\n(Stammdaten → Herkunftshierarchie)");
            }
            return (new Member {
                MgNr = mgnr,
                PredecessorMgNr = json["predecessor_mgnr"]?.AsValue().GetValue<int>(),
                Name = json["name"]?.AsValue().GetValue<string>() ?? json["family_name"]!.AsValue().GetValue<string>(),
                Prefix = json["prefix"]?.AsValue().GetValue<string>(),
                GivenName = json["given_name"]?.AsValue().GetValue<string>(),
                MiddleName = json["middle_names"]?.AsValue().GetValue<string>(),
                Suffix = json["suffix"]?.AsValue().GetValue<string>(),
                ForTheAttentionOf = json["attn"]?.AsValue().GetValue<string>(),
                Birthday = json["birthday"]?.AsValue().GetValue<string>(),
                EntryDateString = json["entry_date"]?.AsValue().GetValue<string>(),
                ExitDateString = json["exit_date"]?.AsValue().GetValue<string>(),
                BusinessShares = json["business_shares"]?.AsValue().GetValue<int>() ?? 0,
                AccountingNr = json["accounting_nr"]?.AsValue().GetValue<string>(),
                ZwstId = json["zwstid"]?.AsValue().GetValue<string>(),
                LfbisNr = json["lfbis_nr"]?.AsValue().GetValue<string>(),
                UstIdNr = json["ustid_nr"]?.AsValue().GetValue<string>(),
                IsJuridicalPerson = json["juridical_pers"]?.AsValue().GetValue<bool>() ?? false,
                IsVollLieferant = json["volllieferant"]?.AsValue().GetValue<bool>() ?? false,
                IsBuchführend = json["buchführend"]?.AsValue().GetValue<bool>() ?? false,
                IsOrganic = json["organic"]?.AsValue().GetValue<bool>() ?? false,
                IsFunktionär = json["funktionär"]?.AsValue().GetValue<bool>() ?? false,
                IsActive = json["active"]?.AsValue().GetValue<bool>() ?? false,
                IsDeceased = json["deceased"]?.AsValue().GetValue<bool>() ?? false,
                Iban = json["iban"]?.AsValue().GetValue<string>(),
                Bic = json["bic"]?.AsValue().GetValue<string>(),
                CountryNum = json["address"]!["country"]!.AsValue().GetValue<int>(),
                PostalDestId = json["address"]!["postal_dest"]!.AsValue().GetValue<string>(),
                Address = json["address"]!["address"]!.AsValue().GetValue<string>(),
                DefaultKgNr = kgnr,
                ContactViaPost = json["contact_postal"]?.AsValue().GetValue<bool>() ?? false,
                ContactViaEmail = json["contact_email"]?.AsValue().GetValue<bool>() ?? false,
                Comment = json["comment"]?.AsValue().GetValue<string>(),
            }, json["billing_address"] is JsonObject a ? new BillingAddr {
                MgNr = mgnr,
                FullName = a["name"]!.AsValue().GetValue<string>(),
                CountryNum = a["country"]!.AsValue().GetValue<int>(),
                PostalDestId = a["postal_dest"]!.AsValue().GetValue<string>(),
                Address = a["address"]!.AsValue().GetValue<string>(),
            } : null, json["telephone_numbers"]!.AsArray().Select(n => n!.AsObject()).Select((n, i) => new MemberTelNr {
                MgNr = mgnr,
                Nr = i + 1,
                Type = n["type"]!.AsValue().GetValue<string>(),
                Number = n["number"]!.AsValue().GetValue<string>(),
                Comment = n["comment"]?.AsValue().GetValue<string>(),
            }).ToList(), json["email_addresses"]!.AsArray().Select(a => a!.AsObject()).Select((a, i) => new MemberEmailAddr {
                MgNr = mgnr,
                Nr = i + 1,
                Address = a["address"]!.AsValue().GetValue<string>(),
                Comment = a["comment"]?.AsValue().GetValue<string>(),
            }).ToList());
        }

        public static JsonObject ToJson(this AreaCom c) {
            return new JsonObject {
                ["fbnr"] = c.FbNr,
                ["mgnr"] = c.MgNr,
                ["vtrgid"] = c.VtrgId,
                ["cultid"] = c.CultId,
                ["area"] = c.Area,
                ["kgnr"] = c.KgNr,
                ["gstnr"] = c.GstNr,
                ["ried"] = c.Rd?.Name,
                ["year_from"] = c.YearFrom,
                ["year_to"] = c.YearTo,
                ["comment"] = c.Comment,
            };
        }

        public static (AreaCom, WbRd?) ToAreaCom(this JsonNode json, Dictionary<int, AT_Kg> kgs, Dictionary<int, List<WbRd>> riede) {
            var kgnr = json["kgnr"]!.AsValue().GetValue<int>();
            var ried = json["ried"]?.AsValue().GetValue<string>();
            WbRd? rd = null;
            bool newRd = false;
            if (ried != null) {
                var rde = riede[kgnr] ?? throw new ArgumentException($"Für KG {(kgs.TryGetValue(kgnr, out var k) ? k.Name : "?")} ({kgnr:00000}) ist noch keine Großlage festgelegt!\n(Stammdaten → Herkunftshierarchie)");
                rd = rde.FirstOrDefault(r => r.Name == ried);
                if (rd == null) {
                    newRd = true;
                    rd = new WbRd {
                        KgNr = kgnr,
                        RdNr = (rde.Count == 0 ? 0 : rde.Max(r => r.RdNr)) + 1,
                        Name = ried,
                    };
                    rde.Add(rd);
                }
            }
            return (new AreaCom {
                FbNr = json["fbnr"]!.AsValue().GetValue<int>(),
                MgNr = json["mgnr"]!.AsValue().GetValue<int>(),
                VtrgId = json["vtrgid"]!.AsValue().GetValue<string>(),
                CultId = json["cultid"]?.AsValue().GetValue<string>(),
                Area = json["area"]!.AsValue().GetValue<int>(),
                KgNr = kgnr,
                GstNr = json["gstnr"]?.AsValue().GetValue<string>() ?? "-",
                RdNr = rd?.RdNr,
                YearFrom = json["year_from"]?.AsValue().GetValue<int>(),
                YearTo = json["year_to"]?.AsValue().GetValue<int>(),
                Comment = json["comment"]?.AsValue().GetValue<string>(),
            }, newRd ? rd : null);
        }

        public static JsonObject ToJson(this Delivery d) {
            return new JsonObject {
                ["lsnr"] = d.LsNr,
                ["year"] = d.Year,
                ["date"] = $"{d.Date:yyyy-MM-dd}",
                ["zwstid"] = d.ZwstId,
                ["lnr"] = d.LNr,
                ["time"] = d.Time != null ? $"{d.Time:HH:mm:ss}" : null,
                ["mgnr"] = d.MgNr,
                ["parts"] = new JsonArray(d.Parts.OrderBy(p => p.DPNr).Select(p => {
                    var obj = new JsonObject {
                        ["dpnr"] = p.DPNr,
                        ["sortid"] = p.SortId,
                        ["attrid"] = p.AttrId,
                        ["cultid"] = p.CultId,
                        ["weight"] = p.Weight,
                        ["kmw"] = p.Kmw,
                        ["qualid"] = p.QualId,
                        ["hkid"] = p.HkId,
                        ["kgnr"] = p.KgNr,
                        ["rdnr"] = p.RdNr,
                        ["net_weight"] = p.IsNetWeight,
                        ["manual_weighing"] = p.IsManualWeighing,
                        ["modids"] = new JsonArray(p.Modifiers.Select(m => (JsonNode)m.ModId).ToArray()),
                        ["comment"] = p.Comment,
                    };
                    if (p.IsSplCheck) obj["spl_check"] = p.IsSplCheck;
                    if (p.IsHandPicked != null) obj["hand_picked"] = p.IsHandPicked;
                    if (p.IsLesewagen != null) obj["lesewagen"] = p.IsLesewagen;
                    if (p.IsGebunden != null) obj["gebunden"] = p.IsGebunden;
                    if (p.Temperature != null) obj["temperature"] = p.Temperature;
                    if (p.Acid != null) obj["acid"] = p.Acid;
                    if (p.ScaleId != null) obj["scale_id"] = p.ScaleId;
                    if (p.WeighingData != null) obj["weighing_data"] = JsonNode.Parse(p.WeighingData);
                    if (p.WeighingReason != null) obj["weighing_reason"] = p.WeighingReason;
                    return obj;
                }).ToArray()),
                ["comment"] = d.Comment,
            };
        }

        public static (Delivery, List<DeliveryPart>, List<DeliveryPartModifier>) ToDelivery(this JsonNode json, Dictionary<string, int> currentLsNrs, Dictionary<int, int> currentDids) {
            var year = json["year"]!.AsValue().GetValue<int>();
            var lsnr = json["lsnr"]!.AsValue().GetValue<string>();
            var did = currentLsNrs.GetValueOrDefault(lsnr, -1);
            if (did == -1) {
                if (!currentDids.ContainsKey(year)) currentDids[year] = 0;
                did = ++currentDids[year];
            }
            currentLsNrs[lsnr] = did;
            return (new Delivery {
                Year = year,
                DId = did,
                DateString = json["date"]!.AsValue().GetValue<string>(),
                TimeString = json["time"]?.AsValue().GetValue<string>(),
                ZwstId = json["zwstid"]!.AsValue().GetValue<string>(),
                LNr = json["lnr"]!.AsValue().GetValue<int>(),
                LsNr = lsnr,
                MgNr = json["mgnr"]!.AsValue().GetValue<int>(),
                Comment = json["comment"]?.AsValue().GetValue<string>(),
            }, json["parts"]!.AsArray().Select(p => p!.AsObject()).Select(p => new DeliveryPart {
                Year = year,
                DId = did,
                DPNr = p["dpnr"]!.AsValue().GetValue<int>(),
                SortId = p["sortid"]!.AsValue().GetValue<string>(),
                AttrId = p["attrid"]?.AsValue().GetValue<string>(),
                CultId = p["cultid"]?.AsValue().GetValue<string>(),
                Weight = p["weight"]!.AsValue().GetValue<int>(),
                Kmw = p["kmw"]!.AsValue().GetValue<double>(),
                QualId = p["qualid"]!.AsValue().GetValue<string>(),
                HkId = p["hkid"]!.AsValue().GetValue<string>(),
                KgNr = p["kgnr"]?.AsValue().GetValue<int>(),
                RdNr = p["rdnr"]?.AsValue().GetValue<int>(),
                IsNetWeight = p["net_weight"]!.AsValue().GetValue<bool>(),
                IsManualWeighing = p["manual_weighing"]!.AsValue().GetValue<bool>(),
                Comment = p["comment"]?.AsValue().GetValue<string>(),
                IsSplCheck = p["spl_check"]?.AsValue().GetValue<bool>() ?? false,
                IsHandPicked = p["hand_picked"]?.AsValue().GetValue<bool>(),
                IsLesewagen = p["lesewagen"]?.AsValue().GetValue<bool>(),
                IsGebunden = p["gebunden"]?.AsValue().GetValue<bool>(),
                Temperature = p["temperature"]?.AsValue().GetValue<double>(),
                Acid = p["acid"]?.AsValue().GetValue<double>(),
                ScaleId = p["scale_id"]?.AsValue().GetValue<string>(),
                WeighingData = p["weighing_data"]?.AsObject().ToJsonString(JsonOpts),
                WeighingReason = p["weighing_reason"]?.AsValue().GetValue<string>(),
            }).ToList(), json["parts"]!.AsArray().SelectMany(p => p!["modids"]!.AsArray().Select(m => new DeliveryPartModifier {
                Year = year,
                DId = did,
                DPNr = p["dpnr"]!.AsValue().GetValue<int>(),
                ModId = m!.AsValue().GetValue<string>(),
            })).ToList());
        }
    }
}