using Elwig.Dialogs;
using Elwig.Documents;
using Elwig.Helpers.Billing;
using Elwig.Models;
using Elwig.Models.Entities;
using LinqKit;
using MailKit.Net.Smtp;
using MailKit.Security;
using Microsoft.EntityFrameworkCore;
using Microsoft.Win32;
using MimeKit;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.IO;
using System.IO.Ports;
using System.Linq;
using System.Linq.Expressions;
using System.Net.Http;
using System.Net.Sockets;
using System.Numerics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Markup;

namespace Elwig.Helpers {
    public static partial class Utils {

        public static readonly Encoding UTF8 = new UTF8Encoding(false, true);
        public static readonly Encoding UTF8BOM = new UTF8Encoding(true, true);
        public static readonly JsonSerializerOptions JsonOpts = new() { Encoder = System.Text.Encodings.Web.JavaScriptEncoder.UnsafeRelaxedJsonEscaping };

        public static int CurrentYear => DateTime.Now.Year;
        public static int CurrentNextSeason => DateTime.Now.Year - (DateTime.Now.Month <= 3 ? 1 : 0);
        public static int CurrentLastSeason => DateTime.Now.Year - (DateTime.Now.Month <= 6 ? 1 : 0);
        public static int FollowingSeason => DateTime.Now.Year + (DateTime.Now.Month >= 11 ? 1 : 0);
        public static DateTime Today => (DateTime.Now.Hour >= 3) ? DateTime.Today : DateTime.Today.AddDays(-1);

        [GeneratedRegex("^serial://([A-Za-z0-9]+):([0-9]+)(,([5-9]),([NOEMSnoems]),(0|1|1\\.5|2|))?$", RegexOptions.Compiled)]
        private static partial Regex GeneratedSerialRegex();
        public static readonly Regex SerialRegex = GeneratedSerialRegex();

        [GeneratedRegex("^tcp://([A-Za-z0-9._-]+):([0-9]+)$", RegexOptions.Compiled)]
        private static partial Regex GeneratedTcpRegex();
        public static readonly Regex TcpRegex = GeneratedTcpRegex();

        [GeneratedRegex(@"^(-?(0?[1-9]|[12][0-9]|3[01])\.(0?[1-9]|1[0-2])\.([0-9]{4})?-?){1,2}$", RegexOptions.Compiled)]
        private static partial Regex GeneratedFromToDateRegex();
        public static readonly Regex DateFromToRegex = GeneratedFromToDateRegex();

        [GeneratedRegex(@"^([0-9]+([\.,][0-9]+)?)?-([0-9]+([\.,][0-9]+)?)?$", RegexOptions.Compiled)]
        private static partial Regex GeneratedFromToRegex();
        public static readonly Regex FromToRegex = GeneratedFromToRegex();

        [GeneratedRegex(@"^([0-9]{1,2}:[0-9]{2})?-([0-9]{1,2}:[0-9]{2})?$", RegexOptions.Compiled)]
        private static partial Regex GeneratedFromToTimeRegex();
        public static readonly Regex FromToTimeRegex = GeneratedFromToTimeRegex();

        [GeneratedRegex(@"^(.*?) +([0-9].*)$", RegexOptions.Compiled)]
        private static partial Regex GeneratedAddressRegex();
        public static readonly Regex AddressRegex = GeneratedAddressRegex();

        [GeneratedRegex(@"[^A-Za-z0-9ÄÜÖẞäöüß-]+")]
        private static partial Regex GeneratedInvalidFileNamePartsRegex();
        public static readonly Regex InvalidFileNamePartsRegex = GeneratedInvalidFileNamePartsRegex();

        public static readonly string GroupSeparator = "\u202F";
        public static readonly string UnitSeparator = "\u00A0";

        public static readonly KeyValuePair<string, string>[] PhoneNrTypes = [
            new("landline", "Tel.-Nr. (Festnetz)"),
            new("mobile", "Tel.-Nr. (mobil)"),
            new("fax", "Fax-Nr."),
        ];

        public static string PhoneNrTypeToString(string type) {
            return PhoneNrTypes.Where(t => t.Key == type).Select(t => t.Value).FirstOrDefault(type);
        }

        private static readonly string[] TempWildcards = ["*.html", "*.pdf", "*.exe", "*.zip"];

        private static readonly ushort[] Crc16ModbusTable = [
            0x0000, 0xC0C1, 0xC181, 0x0140, 0xC301, 0x03C0, 0x0280, 0xC241,
            0xC601, 0x06C0, 0x0780, 0xC741, 0x0500, 0xC5C1, 0xC481, 0x0440,
            0xCC01, 0x0CC0, 0x0D80, 0xCD41, 0x0F00, 0xCFC1, 0xCE81, 0x0E40,
            0x0A00, 0xCAC1, 0xCB81, 0x0B40, 0xC901, 0x09C0, 0x0880, 0xC841,
            0xD801, 0x18C0, 0x1980, 0xD941, 0x1B00, 0xDBC1, 0xDA81, 0x1A40,
            0x1E00, 0xDEC1, 0xDF81, 0x1F40, 0xDD01, 0x1DC0, 0x1C80, 0xDC41,
            0x1400, 0xD4C1, 0xD581, 0x1540, 0xD701, 0x17C0, 0x1680, 0xD641,
            0xD201, 0x12C0, 0x1380, 0xD341, 0x1100, 0xD1C1, 0xD081, 0x1040,
            0xF001, 0x30C0, 0x3180, 0xF141, 0x3300, 0xF3C1, 0xF281, 0x3240,
            0x3600, 0xF6C1, 0xF781, 0x3740, 0xF501, 0x35C0, 0x3480, 0xF441,
            0x3C00, 0xFCC1, 0xFD81, 0x3D40, 0xFF01, 0x3FC0, 0x3E80, 0xFE41,
            0xFA01, 0x3AC0, 0x3B80, 0xFB41, 0x3900, 0xF9C1, 0xF881, 0x3840,
            0x2800, 0xE8C1, 0xE981, 0x2940, 0xEB01, 0x2BC0, 0x2A80, 0xEA41,
            0xEE01, 0x2EC0, 0x2F80, 0xEF41, 0x2D00, 0xEDC1, 0xEC81, 0x2C40,
            0xE401, 0x24C0, 0x2580, 0xE541, 0x2700, 0xE7C1, 0xE681, 0x2640,
            0x2200, 0xE2C1, 0xE381, 0x2340, 0xE101, 0x21C0, 0x2080, 0xE041,
            0xA001, 0x60C0, 0x6180, 0xA141, 0x6300, 0xA3C1, 0xA281, 0x6240,
            0x6600, 0xA6C1, 0xA781, 0x6740, 0xA501, 0x65C0, 0x6480, 0xA441,
            0x6C00, 0xACC1, 0xAD81, 0x6D40, 0xAF01, 0x6FC0, 0x6E80, 0xAE41,
            0xAA01, 0x6AC0, 0x6B80, 0xAB41, 0x6900, 0xA9C1, 0xA881, 0x6840,
            0x7800, 0xB8C1, 0xB981, 0x7940, 0xBB01, 0x7BC0, 0x7A80, 0xBA41,
            0xBE01, 0x7EC0, 0x7F80, 0xBF41, 0x7D00, 0xBDC1, 0xBC81, 0x7C40,
            0xB401, 0x74C0, 0x7580, 0xB541, 0x7700, 0xB7C1, 0xB681, 0x7640,
            0x7200, 0xB2C1, 0xB381, 0x7340, 0xB101, 0x71C0, 0x7080, 0xB041,
            0x5000, 0x90C1, 0x9181, 0x5140, 0x9301, 0x53C0, 0x5280, 0x9241,
            0x9601, 0x56C0, 0x5780, 0x9741, 0x5500, 0x95C1, 0x9481, 0x5440,
            0x9C01, 0x5CC0, 0x5D80, 0x9D41, 0x5F00, 0x9FC1, 0x9E81, 0x5E40,
            0x5A00, 0x9AC1, 0x9B81, 0x5B40, 0x9901, 0x59C0, 0x5880, 0x9841,
            0x8801, 0x48C0, 0x4980, 0x8941, 0x4B00, 0x8BC1, 0x8A81, 0x4A40,
            0x4E00, 0x8EC1, 0x8F81, 0x4F40, 0x8D01, 0x4DC0, 0x4C80, 0x8C41,
            0x4400, 0x84C1, 0x8581, 0x4540, 0x8701, 0x47C0, 0x4680, 0x8641,
            0x8201, 0x42C0, 0x4380, 0x8341, 0x4100, 0x81C1, 0x8081, 0x4040,
        ];

        public static void OverrideCulture() {
            var locale = new CultureInfo("de-AT", false);
            locale.NumberFormat.CurrencyGroupSeparator = Utils.GroupSeparator;
            locale.NumberFormat.NumberGroupSeparator = Utils.GroupSeparator;
            locale.NumberFormat.PercentGroupSeparator = Utils.GroupSeparator;
            CultureInfo.CurrentCulture = locale;
            CultureInfo.CurrentUICulture = locale;
            Thread.CurrentThread.CurrentCulture = locale;
            Thread.CurrentThread.CurrentUICulture = locale;
            CultureInfo.DefaultThreadCurrentCulture = locale;
            CultureInfo.DefaultThreadCurrentUICulture = locale;
            FrameworkElement.LanguageProperty.OverrideMetadata(
                typeof(FrameworkElement),
                new FrameworkPropertyMetadata(XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.Name))
            );
        }

        public static SerialPort OpenSerialConnection(string connection) {
            var m = SerialRegex.Match(connection);
            if (!m.Success)
                throw new ArgumentException("Invalid connection string for scheme \"serial\"");

            var stop = m.Groups[6].Value;
            var parity = m.Groups[5].Value.ToUpper();
            var data = m.Groups[4].Value;
            var port = new SerialPort() {
                PortName = m.Groups[1].Value,
                BaudRate = int.Parse(m.Groups[2].Value),
                Parity = parity == "E" ? Parity.Even :
                         parity == "O" ? Parity.Odd :
                         parity == "M" ? Parity.Mark :
                         parity == "S" ? Parity.Space :
                                         Parity.None,
                DataBits = data == "" ? 8 : int.Parse(data),
                StopBits = (StopBits)(stop == "" ? 1 : stop == "1.5" ? 3 : stop[0] - '0'),
                Handshake = Handshake.None,
                WriteTimeout = 250,
                ReadTimeout = 11000,
            };
            port.Open();
            return port;
        }

        public static TcpClient OpenTcpConnection(string connection) {
            var m = TcpRegex.Match(connection);
            if (!m.Success)
                throw new ArgumentException("Invalid connection string for scheme \"tcp\"");

            var client = new TcpClient() {
                SendTimeout = 250,
                ReceiveTimeout = 11000,
            };
            client.Connect(m.Groups[1].Value, int.Parse(m.Groups[2].Value));
            return client;
        }

        public static int Modulo(string a, int b) {
            if (a.Length == 0 || !a.All(char.IsAsciiDigit)) {
                throw new ArgumentException("First argument has to be a decimal string");
            } else if (b < 2) {
                throw new ArgumentException("Second argument has to be greater than 1");
            }
            return a.Select(ch => ch - '0').Aggregate((sum, n) => (sum * 10 + n) % b);
        }

        public static ushort CalcCrc16Modbus(byte[] data) {
            // https://www.modbustools.com/modbus_crc16.htm
            byte temp;
            ushort crc = 0xFFFF;
            foreach (byte b in data) {
                temp = (byte)(b ^ crc);
                crc >>= 8;
                crc ^= Crc16ModbusTable[temp];
            }
            return crc;
        }

        public static ushort CalcCrc16Modbus(string data) {
            return CalcCrc16Modbus(Encoding.ASCII.GetBytes(data));
        }

        public static string FormatIban(string iban) {
            return Regex.Replace(iban.Trim(), ".{4}", "$0 ").Trim();
        }

        public static void RunBackground(string title, Func<Task> a) {
            Task.Run(async () => {
                try {
                    await a();
                } catch (Exception e) {
                    MessageBox.Show(e.ToString(), title, MessageBoxButton.OK, MessageBoxImage.Error);
                }
            });
        }

        public static void MailTo(string emailAddress) {
            MailTo([emailAddress]);
        }

        public static void MailTo(IEnumerable<string> emailAddresses) {
            Process.Start(new ProcessStartInfo() {
                FileName = $"mailto:{string.Join(",%20", emailAddresses)}",
                UseShellExecute = true,
            });
        }

        public static double KmwToOe(double kmw) {
            return Math.Round(kmw * (4.54 + 0.022 * kmw), 0);
        }

        public static double OeToKmw(double oe) {
            return Math.Round((-4.54 + Math.Sqrt(4.54 * 4.54 - 4 * 0.022 * -oe)) / (2 * 0.022), 1);
        }

        public static decimal DecFromDb(long value, byte precision) {
            bool neg = value < 0;
            if (neg) value = -value;
            return new((int)(value & 0xFFFFFFFF), (int)((value >> 32) & 0x7FFFFFFF), 0, neg, precision);
        }

        public static long DecToDb(decimal value, byte precision) {
            return (long)decimal.Round(value * (decimal)Math.Pow(10, precision), 0);
        }

        public static int GetAge(DateOnly birthday) {
            var today = DateTime.Today;
            var a = (today.Year * 100 + today.Month) * 100 + today.Day;
            var b = (birthday.Year * 100 + birthday.Month) * 100 + birthday.Day;
            return (a - b) / 10000;
        }

        public static int GetSearchScore(IEnumerable<string?> words, IEnumerable<string> searchKeywords) {
            searchKeywords = searchKeywords.Where(s => s.Length >= 2 || (s.Length > 0 && s.All(c => char.IsAsciiDigit(c))));
            if (!searchKeywords.Any())
                return 0;

            return searchKeywords
                .Select(k => {
                    k = k.ToLower();
                    var scores = words.Select(w => {
                        w = w?.ToLower();
                        var p = w?.ToLower()?.Split(" ");
                        if (w == null || p == null) {
                            return 0;
                        } else if (k == w) {
                            return 4 + k.Length;
                        } else if (p.Any(a => a == k)) {
                            return 3 + k.Length;
                        } else if (p.Any(a => a.StartsWith(k))) {
                            return 2 + k.Length;
                        } else if (w.Contains(k)) {
                            return 1 + k.Length;
                        } else {
                            return 0;
                        }
                    });
                    return scores.Max() + scores.Count(s => s > 0);
                })
                .Sum();
        }

        public static (int, string?)? ShowManualWeighingDialog(string? reason = null) {
            var d = new ManualWeighingDialog(reason);
            return d.ShowDialog() == true ? (d.Weight, d.Reason) : null;
        }

        public static (string?, int[])? ShowDeliverySplittingDialog(Delivery delivery) {
            var d = new DeliverySplittingDialog(delivery);
            return d.ShowDialog() == true ? (d.MgNr?.ToString() ?? d.LsNr, d.Weights ?? []) : null;
        }

        public static double? ShowLinearPriceIncreaseDialog() {
            var d = new LinearPriceIncreaseDialog();
            return d.ShowDialog() == true ? d.Price : null;
        }

        public static Footer GenerateFooter(string lineBreak, string seperator) {
            return new Footer(lineBreak, seperator);
        }

        public class Footer {
            private string Text = "";
            private readonly string LineBreak;
            private readonly string Seperator;
            private bool FirstLine = true;
            private bool FirstItemInLine = true;

            public Footer(string lineBreak, string seperator) {
                LineBreak = lineBreak;
                Seperator = seperator;
            }

            public Footer Item(string? text) {
                if (text == null) return this;
                Text += FirstItemInLine ? (FirstLine ? "" : LineBreak) : Seperator;
                Text += text;
                FirstLine = false;
                FirstItemInLine = false;
                return this;
            }

            public Footer Item(string name, string? text) {
                return text == null ? this : Item($"{name}: {text}");
            }

            public Footer NextLine() {
                FirstItemInLine = true;
                return this;
            }

            public override string ToString() {
                return Text;
            }
        }

        public static string GetSign<T>(T number) where T : INumber<T>
            => T.Sign(number) switch {
                < 0 => "\u2212",  // minus
                  0 => "\u00b1",  // plus minus
                > 0 => "+",
            };

        public static double AggregateDeliveryPartsKmw(IEnumerable<IDelivery> parts)
            => parts.Aggregate(
                (Weight: 0, Kmw: 0.0),
                (sum, item) => (
                    sum.Weight + item.Weight,
                    (sum.Kmw * sum.Weight + item.Kmw * item.Weight) / (sum.Weight + item.Weight)
                ),
                sum => sum.Kmw
            );

        public static string GenerateLsNr(Delivery d) => GenerateLsNr(d.Date, d.ZwstId, d.LNr);

        public static string GenerateLsNr(DateOnly date, string zwstid, int lnr) => $"{date:yyyyMMdd}{zwstid}{lnr:000}";

        public static (string, string?) SplitAddress(string address) {
            var m = AddressRegex.Match(address);
            return (m.Groups[1].Value, m.Groups[2].Value);
        }

        public static (string, string?) SplitName(string fullName, string? familyName) {
            if (string.IsNullOrWhiteSpace(familyName)) return (fullName, null);
            var p0 = fullName.IndexOf(familyName, StringComparison.CurrentCultureIgnoreCase);
            if (p0 == -1) return (fullName, null);
            var p1 = fullName.IndexOf(" und ");
            var p2 = fullName.ToLower().LastIndexOf(" und ");
            if (p1 != p2) {
                if (p0 > p1) {
                    // A und B familyName [und ...]
                    return (fullName[p0..^0], fullName[0..(p0 - 1)]);
                } else {
                    // familyName und ... A und B
                    var p3 = fullName.LastIndexOf(' ', p2 - 1);
                    return (fullName[0..p3], fullName[(p3 + 1)..^0]);
                }
            } else if (p0 + familyName.Length >= fullName.Length || fullName[p0 + familyName.Length] == ' ') {
                return (familyName, fullName.Replace(familyName, "").Replace("  ", " ").Trim());
            } else {
                return (fullName, null);
            }
        }

        public static IEnumerable<IEnumerable<T>> Permutate<T>(IEnumerable<T> input, IEnumerable<T>? forced = null) {
            HashSet<IEnumerable<T>> output = [];
            for (int i = 0; i < Math.Pow(2, input.Count()); i++) {
                List<T> t = [];
                for (int j = 0; j < 30; j++) {
                    var e = input.ElementAtOrDefault(j);
                    if (e != null && ((forced?.Contains(e) ?? false) || (i & (1 << j)) != 0)) {
                        t.Add(e);
                    }
                }
                output.Add(t);
            }
            return output.OrderByDescending(l => l.Count());
        }

        public static List<RawVaribute> GetVaributes(AppDbContext ctx, int year, bool onlyDelivered = true) {
            var varieties = ctx.WineVarieties.Select(v => new RawVaribute(v.SortId, "", null)).ToList();
            var delivered = ctx.DeliveryParts
               .Where(d => d.Year == year)
               .Select(d => new RawVaribute(d.SortId, d.AttrId ?? "", d.CultId ?? ""))
               .Distinct()
               .ToList();
            return [.. (onlyDelivered ? delivered : delivered.Union(varieties)).Order()];
        }

        public static List<Varibute> GetVaributeList(AppDbContext ctx, int year, bool onlyDelivered = true) {
            var varieties = ctx.WineVarieties.ToDictionary(v => v.SortId, v => v);
            var attributes = ctx.WineAttributes.ToDictionary(a => a.AttrId, a => a);
            var cultivations = ctx.WineCultivations.ToDictionary(c => c.CultId, c => c);
            return GetVaributes(ctx, year, onlyDelivered)
                .Select(s => new Varibute(s, varieties, attributes, cultivations))
                .ToList();
        }

        [LibraryImport("wininet.dll")]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static partial bool InternetGetConnectedState(out int description, int reservedValue);

        public static bool HasInternetConnectivity() {
            return InternetGetConnectedState(out var _, 0);
        }

        public static HttpClient GetHttpClient(string? username = null, string? password = null, string? accept = null) {
            var client = new HttpClient() {
                Timeout = TimeSpan.FromSeconds(5),
            };
            client.DefaultRequestHeaders.UserAgent.Clear();
            client.DefaultRequestHeaders.UserAgent.ParseAdd($"Elwig/{App.Version} ({App.Client.NameToken}, {App.BranchName}, {Environment.MachineName}, {Environment.OSVersion})");
            client.DefaultRequestHeaders.Accept.Clear();
            if (accept != null)
                client.DefaultRequestHeaders.Accept.Add(new(accept));
            if (username != null || password != null)
                client.DefaultRequestHeaders.Authorization = new("Basic", Convert.ToBase64String(Utils.UTF8.GetBytes($"{username}:{password}")));
            return client;
        }

        public static async Task<(string Version, string Url, long Size)?> GetLatestInstallerUrl(string url) {
            try {
                using var client = GetHttpClient(accept: "application/json");
                using var res = await client.GetAsync(url);
                if (!res.IsSuccessStatusCode)
                    return null;
                var resJson = JsonNode.Parse(await res.Content.ReadAsStringAsync());
                var data = resJson!["data"]!.AsArray()[^1]!;
                return ((string)data["version"]!, (string)data["url"]!, (long)data["size"]!);
            } catch {
                return null;
            }
        }

        public static async Task UploadExportData(string zip, string url, string username, string password) {
            if (url.StartsWith("https://elwig.at/clients/"))
                url = "https://sync.elwig.at/" + url[25..];
            if (!url.EndsWith('/')) url += "/";
            using var client = GetHttpClient(username, password, accept: "application/json");
            using var content = new StreamContent(new FileStream(zip, FileMode.Open, FileAccess.Read));
            content.Headers.ContentType = new("application/zip");
            using var res = await client.PutAsync(url + Path.GetFileName(zip), content);
            res.EnsureSuccessStatusCode();
        }

        public static async Task<JsonArray> GetExportMetaData(string url, string username, string password) {
            if (url.StartsWith("https://elwig.at/clients/"))
                url = "https://sync.elwig.at/" + url[25..];
            using var client = GetHttpClient(username, password, accept: "application/json");
            using var res = await client.GetAsync(url);
            res.EnsureSuccessStatusCode();
            var resJson = JsonNode.Parse(await res.Content.ReadAsStringAsync());
            var data = resJson!["data"]!;
            return data.AsArray();
        }

        public static void CleanupTempFiles() {
            var dir = new DirectoryInfo(App.TempPath);
            foreach (var file in TempWildcards.SelectMany(dir.EnumerateFiles)) {
                file.Delete();
            }
        }

        public static string NormalizeFileName(string filename) {
            return InvalidFileNamePartsRegex.Replace(filename.Replace('/', '-'), "_");
        }

        public static async Task<SmtpClient?> GetSmtpClient() {
            if (App.Config.Smtp == null)
                return null;
            var (host, port, mode, username, password, _) = App.Config.Smtp.Value;
            var client = new SmtpClient();
            await client.ConnectAsync(host, port, mode == "starttls" ? SecureSocketOptions.StartTls : SecureSocketOptions.None);
            await client.AuthenticateAsync(username, password);
            return client;
        }

        public static async Task<bool> SendEmail(Member member, string subject, string text, IEnumerable<Document> docs) {
            if (App.Config.Smtp == null)
                return false;
            return await Task.Run(async () => {
                await AddSentMailBody(subject, text, 1);
                SmtpClient? client = null;
                try {
                    client = await GetSmtpClient();

                    using var msg = new MimeMessage();
                    msg.From.Add(new MailboxAddress(App.Client.NameFull, App.Config.Smtp.Value.From));
                    msg.To.AddRange(member.EmailAddresses.OrderBy(a => a.Nr).Select(a => new MailboxAddress(member.AdministrativeName, a.Address)));
                    msg.Subject = subject;
                    var body = new Multipart("mixed") {
                        new TextPart("plain") { Text = text }
                    };
                    foreach (var doc in docs) {
                        var name = NormalizeFileName(doc.Title);
                        body.Add(doc.AsEmailAttachment($"{name}.pdf"));
                    }
                    msg.Body = body;
                    await client!.SendAsync(msg);
                    await AddSentMails([(
                            "email", member.MgNr, member.AdministrativeName,
                            member.EmailAddresses.OrderBy(a => a.Nr).Select(a => a.Address).ToArray(),
                            subject, docs.Select(d => d.Title).ToArray()
                        )]);
                } catch (Exception exc) {
                    MessageBox.Show(exc.Message, "Fehler", MessageBoxButton.OK, MessageBoxImage.Error);
                    return false;
                } finally {
                    if (client != null)
                        await client.DisconnectAsync(true);
                    client?.Dispose();
                }
                return true;
            });
        }

        public static async Task ExportDocument(Document doc, ExportMode mode, string? filename = null, (Member Member, string Subject, string Text)? emailData = null) {
            if (mode == ExportMode.Print && !App.Config.Debug) {
                if (doc.IsPreview) {
                    MessageBox.Show("Dieses Dokument ist als vorläufig markiert und kann daher nicht ausgedruckt werden!",
                        "Vorläufiges Dokument", MessageBoxButton.OK, MessageBoxImage.Error);
                    return;
                }
                await doc.Generate();
                await doc.Print();
            } else if (mode == ExportMode.Email && emailData is (Member, string, string) e) {
                if (doc.IsPreview) {
                    MessageBox.Show("Dieses Dokument ist als vorläufig markiert und kann daher nicht verschickt werden!",
                        "Vorläufiges Dokument", MessageBoxButton.OK, MessageBoxImage.Error);
                    return;
                }
                await doc.Generate();
                var success = await SendEmail(e.Member, e.Subject, e.Text, [doc]);
                if (success)
                    MessageBox.Show("Die E-Mail wurde erfolgreich verschickt!\n\nEs kann einige Minuten dauern, bis die E-Mail im Posteingang des Empfängers aufscheint.", "E-Mail verschickt",
                        MessageBoxButton.OK, MessageBoxImage.Information);
            } else if (mode == ExportMode.SavePdf) {
                if (doc.IsPreview) {
                    MessageBox.Show("Dieses Dokument ist als vorläufig markiert und sollte daher nicht langfristig gespeichert werden!",
                        "Vorläufiges Dokument", MessageBoxButton.OK, MessageBoxImage.Warning);
                }
                var d = new SaveFileDialog() {
                    FileName = $"{NormalizeFileName(filename ?? doc.Title)}.pdf",
                    DefaultExt = "pdf",
                    Filter = "PDF-Datei (*.pdf)|*.pdf",
                    Title = $"{doc.Title} speichern unter - Elwig"
                };
                if (d.ShowDialog() == true) {
                    await doc.Generate();
                    doc.SaveTo(d.FileName);
                    Process.Start("explorer.exe", d.FileName);
                }
            } else {
                await doc.Generate();
                doc.Show();
            }
        }

        public static List<EventLogEntry> GetLogEntries() {
            using var log = new EventLog {
                Log = "Application",
                Source = ".NET Runtime",
            };
            return [.. log.Entries.OfType<EventLogEntry>().Where(e => e.InstanceId == 1026).Where(e => e.Message.StartsWith("Application: Elwig.exe"))];
        }

        public static int GetEntityIdetifierForPk(params object?[] primaryKey) {
            var pk = primaryKey.Select(k => k?.GetHashCode() ?? 0).ToArray();
            return ((IStructuralEquatable)pk).GetHashCode(EqualityComparer<int>.Default);
        }

        public static int? GetEntityIdentifier(object? obj) {
            if (obj == null) {
                return null;
            } else if (obj is IEnumerable list && obj is not string) {
                var arr = list.Cast<object>().Select(GetEntityIdentifier).ToArray();
                return ((IStructuralEquatable)arr).GetHashCode(EqualityComparer<int?>.Default);
            } else if (obj.GetType().GetCustomAttribute(typeof(PrimaryKeyAttribute), true) is PrimaryKeyAttribute pkAttr) {
                var pk = pkAttr.PropertyNames.Select(name => obj.GetType().GetProperty(name)!.GetValue(obj)?.GetHashCode() ?? 0).ToArray();
                return ((IStructuralEquatable)pk).GetHashCode(EqualityComparer<int>.Default);
            } else {
                return obj.GetHashCode();
            }
        }

        public static Expression<Func<AreaCom, bool>> ActiveAreaCommitments() => ActiveAreaCommitments(CurrentYear);
        public static Expression<Func<AreaCom, bool>> ActiveAreaCommitments(int year) =>
            c => (c.YearFrom == null || c.YearFrom <= year) && (c.YearTo == null || c.YearTo >= year);

        public static IQueryable<AreaCom> ActiveAreaCommitments(IQueryable<AreaCom> query) => ActiveAreaCommitments(query, CurrentYear);
        public static IQueryable<AreaCom> ActiveAreaCommitments(IQueryable<AreaCom> query, int year) =>
            query.Where(ActiveAreaCommitments(year));

        public static IEnumerable<AreaCom> ActiveAreaCommitments(IEnumerable<AreaCom> query) => ActiveAreaCommitments(query, CurrentYear);
        public static IEnumerable<AreaCom> ActiveAreaCommitments(IEnumerable<AreaCom> query, int year) =>
            query.Where(c => ActiveAreaCommitments(year).Invoke(c));

        public static async Task<(DateTime DateTime, string Type, int MgNr, string Name, string[] Addresses, string Subject, string[] Attachments)[]> GetSentMails(DateOnly? fromDate = null, DateOnly? toDate = null) {
            var f = $"{fromDate:yyyy-MM-dd}";
            var t = $"{toDate:yyyy-MM-dd}";
            try {
                var rows = new List<(DateTime, string, int, string, string[], string, string[])>();
                var filenames = Directory.GetFiles(App.MailsPath, "????.csv")
                    .Where(n => fromDate == null || Path.GetFileName(n).CompareTo($"{fromDate.Value.Year}.csv") >= 0)
                    .Where(n => toDate   == null || Path.GetFileName(n).CompareTo($"{toDate.Value.Year}.csv")   <= 0)
                    .Order();
                foreach (var filename in filenames) {
                    using var reader = new StreamReader(filename, Utils.UTF8);
                    string? line;
                    while ((line = await reader.ReadLineAsync()) != null) {
                        try {
                            if (line.Length < 20 || line[10] != ';' || line[19] != ';')
                                continue;
                            var date = line[..10];
                            if (fromDate != null && date.CompareTo(f) < 0) {
                                continue;
                            } else if (toDate != null && date.CompareTo(t) > 0) {
                                break;
                            }
                            var p = line.Split(';');
                            rows.Add((
                                DateOnly.ParseExact(p[0], "yyyy-MM-dd").ToDateTime(TimeOnly.ParseExact(p[1], "HH:mm:ss")),
                                p[2],
                                int.Parse(p[3]),
                                p[4],
                                p[5].Split(',').Select(a => a.Replace(" | ", "\n")).ToArray(),
                                p[6],
                                p[7].Split(',')
                            ));
                        } catch {
                            continue;
                        }
                    }
                }
                return [.. rows];
            } catch {
                return [];
            }
        }

        public static async Task AddSentMails(IEnumerable<(string Type, int MgNr, string Name, string[] Addresses, string Subject, string[] Attachments)> data) {
            var now = DateTime.Now;
            var filename = Path.Combine(App.MailsPath, $"{now.Year}.csv");
            await File.AppendAllLinesAsync(filename, data.Select(d =>
                $"{now:yyyy-MM-dd;HH:mm:ss};{d.Type};" +
                $"{d.MgNr};{d.Name.Replace(';', ' ')};" +
                $"{string.Join(',', d.Addresses.Select(a => a.Replace(';', ' ').Replace(',', ' ').Replace("\n", " | ")))};" +
                $"{d.Subject.Replace(';', ' ')};" +
                $"{string.Join(',', d.Attachments.Select(a => a.Replace(';', ' ').Replace(',', ' ')))}"
            ), Utils.UTF8);
        }

        public static async Task<string?> FindSentMailBody(DateTime target) {
            var dt = $"{target:yyyy-MM-dd_HH-mm-ss}";
            var filename = Directory.GetFiles(App.MailsPath, "????-??-??_??-??-??_*.txt")
                .Where(n => Path.GetFileName(n)[..19].CompareTo(dt) <= 0)
                .Order()
                .LastOrDefault();
            if (filename == null)
                return null;
            return await File.ReadAllTextAsync(filename, Utils.UTF8);
        }

        public static async Task AddSentMailBody(string subject, string body, int recipients) {
            var filename = Path.Combine(App.MailsPath, $"{DateTime.Now:yyyy-MM-dd_HH-mm-ss}_{NormalizeFileName(subject)}.txt");
            await File.WriteAllTextAsync(filename, $"# {subject}\r\n# Vorgesehene Empfänger: {recipients}\r\n\r\n" + body, Utils.UTF8);
        }
    }
}
