using Microsoft.EntityFrameworkCore;
using Elwig.Models.Entities;
using System.Linq;
using System.Threading.Tasks;
using System.IO;
using System;
using System.Windows;
using Microsoft.Extensions.Logging;
using Microsoft.Data.Sqlite;
using System.Text.RegularExpressions;
using System.Collections.Generic;
using Elwig.Models.Dtos;
using System.Reflection;
using System.Data;

namespace Elwig.Helpers {

    public record struct AreaComBucket(int Area, int Obligation, int Right);
    public record struct UnderDelivery(int Weight, int Diff);
    public record struct MemberBucket(string Name, int Area, int Obligation, int Right, int Delivery, int DeliveryStrict, int Payment);
    public record struct MemberStat(string Variety, string Discr, int Weight);

    public class AppDbContext : DbContext {

        public DbSet<Country> Countries { get; private set; }
        public DbSet<Currency> Currencies { get; private set; }
        public DbSet<AT_Gem> Gemeinden { get; private set; }
        public DbSet<AT_Kg> Katastralgemeinden { get; private set; }
        public DbSet<AT_Ort> Orte { get; private set; }
        public DbSet<AT_Plz> Postleitzahlen { get; private set; }
        public DbSet<AT_PlzDest> PlzDestinations { get; private set; }
        public DbSet<PostalDest> PostalDestinations { get; private set; }
        public DbSet<WineOrigin> WineOrigins { get; private set; }
        public DbSet<WineQualLevel> WineQualityLevels { get; private set; }
        public DbSet<WineVar> WineVarieties { get; private set; }

        public DbSet<ClientParam> ClientParameters { get; private set; }
        public DbSet<WbGl> WbGls { get; private set; }
        public DbSet<WbGem> WbGems { get; private set; }
        public DbSet<WbKg> WbKgs { get; private set; }
        public DbSet<WbRd> WbRde { get; private set; }
        public DbSet<WineAttr> WineAttributes { get; private set; }
        public DbSet<WineCult> WineCultivations { get; private set; }
        public DbSet<Branch> Branches { get; private set; }
        public DbSet<AreaComType> AreaCommitmentTypes { get; private set; }
        public DbSet<Member> Members { get; private set; }
        public DbSet<BillingAddr> BillingAddresses { get; private set; }
        public DbSet<MemberTelNr> MemberTelephoneNrs { get; private set; }
        public DbSet<MemberHistory> MemberHistory { get; private set; }
        public DbSet<AreaCom> AreaCommitments { get; private set; }
        public DbSet<Season> Seasons { get; private set; }
        public DbSet<Modifier> Modifiers { get; private set; }
        public DbSet<Delivery> Deliveries { get; private set; }
        public DbSet<DeliveryPart> DeliveryParts { get; private set; }
        public DbSet<DeliveryPartModifier> DeliveryPartModifiers { get; private set; }
        public DbSet<PaymentVar> PaymentVariants { get; private set; }
        public DbSet<PaymentMember> MemberPayments { get; private set; }
        public DbSet<Credit> Credits { get; private set; }

        public DbSet<OverUnderDeliveryRow> OverUnderDeliveryRows { get; private set; }
        public DbSet<AreaComUnderDeliveryRowSingle> AreaComUnderDeliveryRows { get; private set; }
        public DbSet<MemberDeliveryPerVariantRowSingle> MemberDeliveryPerVariantRows { get; private set; }
        public DbSet<CreditNoteDeliveryRowSingle> CreditNoteDeliveryRows { get; private set; }
        public DbSet<CreditNoteRowSingle> CreditNoteRows { get; private set; }

        private readonly StreamWriter? LogFile = null;
        public static DateTime LastWriteTime => File.GetLastWriteTime(App.Config.DatabaseFile);
        public DateTime SavedLastWriteTime { get; private set; }
        public bool HasBackendChanged => SavedLastWriteTime != LastWriteTime;

        public static string? ConnectionStringOverride { get; set; } = null;
        public static string ConnectionString => ConnectionStringOverride ?? $"Data Source=\"{App.Config.DatabaseFile}\"; Mode=ReadWrite; Foreign Keys=True; Cache=Default";

        private readonly Dictionary<int, Dictionary<int, Dictionary<string, AreaComBucket>>> _memberAreaCommitmentBuckets = [];
        private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBuckets = [];
        private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberDeliveryBucketsStrict = [];
        private readonly Dictionary<int, Dictionary<int, Dictionary<string, int>>> _memberPaymentBuckets = [];
        private readonly Dictionary<int, Dictionary<int, Dictionary<string, UnderDelivery>>> _memberUnderDelivery = [];

        public AppDbContext() {
            if (App.Config.DatabaseLog != null) {
                try {
                    var file = File.Open(App.Config.DatabaseLog, FileMode.Append, FileAccess.Write, FileShare.Write);
                    LogFile = new(file) {
                        AutoFlush = true
                    };
                } catch (Exception e) {
                    MessageBox.Show($"Unable to open database log file:\n\n{e.Message}", "Database Log", MessageBoxButton.OK, MessageBoxImage.Error);
                }
            }
            SavedLastWriteTime = LastWriteTime;
            SavedChanges += OnSavedChanges;
        }

        public static SqliteConnection Connect() {
            var cnx = new SqliteConnection(ConnectionString);
            cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => value == null ? null : Regex.Match(value, pattern).Success, true);
            cnx.Open();
            return cnx;
        }

        public static async Task<SqliteConnection> ConnectAsync() {
            var cnx = new SqliteConnection(ConnectionString);
            cnx.CreateFunction<string, string?, bool?>("REGEXP", (pattern, value) => value == null ? null : Regex.Match(value, pattern).Success, true);
            await cnx.OpenAsync();
            return cnx;
        }

        public static async Task ExecuteBatch(SqliteConnection cnx, string sql) {
            using var cmd = cnx.CreateCommand();
            cmd.CommandText = sql;
            await (await cmd.ExecuteReaderAsync()).CloseAsync();
        }

        public static async Task ExecuteEmbeddedScript(SqliteConnection cnx, Assembly asm, string name) {
            using var stream = asm.GetManifestResourceStream(name) ?? throw new FileNotFoundException("Unable to load embedded resource");
            using var reader = new StreamReader(stream);
            await ExecuteBatch(cnx, await reader.ReadToEndAsync());
        }

        public static async Task<object?> ExecuteScalar(SqliteConnection cnx, string sql) {
            using var cmd = cnx.CreateCommand();
            cmd.CommandText = sql;
            return await cmd.ExecuteScalarAsync();
        }

        public static async Task<(string Table, long RowId, string Parent, long FkId)[]> ForeignKeyCheck(SqliteConnection cnx) {
            using var cmd = cnx.CreateCommand();
            cmd.CommandText = "PRAGMA foreign_key_check";
            using var reader = await cmd.ExecuteReaderAsync();
            var list = new List<(string, long, string, long)>();
            while (await reader.ReadAsync()) {
                var table = reader.GetString(0);
                var rowid = reader.GetInt64(1);
                var parent = reader.GetString(2);
                var fkid = reader.GetInt64(3);
                list.Add((table, rowid, parent, fkid));
            }
            return [.. list];
        }

        protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) {
            optionsBuilder.UseSqlite(ConnectionString);
            optionsBuilder.UseLazyLoadingProxies();
            optionsBuilder.LogTo(Log, LogLevel.Information);
            base.OnConfiguring(optionsBuilder);
        }

        public override void Dispose() {
            base.Dispose();
            LogFile?.Dispose();
            GC.SuppressFinalize(this);
        }

        private void OnSavedChanges(object? sender, SavedChangesEventArgs evt) {
            SavedLastWriteTime = LastWriteTime;
        }

        protected void Log(string msg) {
            LogFile?.WriteLine(msg);
        }

        public async Task<bool> MgNrExists(int mgnr) {
            return await Members.FindAsync(mgnr) != null;
        }

        public async Task<bool> FbNrExists(int fbnr) {
            return await AreaCommitments.FindAsync(fbnr) != null;
        }

        public async Task<bool> SortIdExists(string sortId) {
            return await WineVarieties.FindAsync(sortId) != null;
        }

        public async Task<bool> AttrIdExists(string attrId) {
            return await WineAttributes.FindAsync(attrId) != null;
        }

        public async Task<bool> CultIdExists(string cultId) {
            return await WineCultivations.FindAsync(cultId) != null;
        }

        public async Task<int> NextMgNr() {
            int c = 0;
            (await Members.OrderBy(m => m.MgNr).Select(m => m.MgNr).ToListAsync())
                .ForEach(a => { if (a <= c + 1000) c = a; });
            return c + 1;
        }

        public async Task<int> NextFbNr() {
            int c = 0;
            (await AreaCommitments.OrderBy(ac => ac.FbNr).Select(ac => ac.FbNr).ToListAsync())
                .ForEach(a => { if (a <= c + 10000) c = a; });
            return c + 1;
        }

        public async Task<int> NextLNr(DateOnly date) {
            var dateStr = date.ToString("yyyy-MM-dd");
            int c = 0;
            (await Deliveries.Where(d => d.DateString == dateStr).Select(d => d.LNr).ToListAsync())
                .ForEach(a => { if (a <= c + 100) c = a; });
            return c + 1;
        }

        public async Task<int> NextDId(int year) {
            int c = 0;
            (await Deliveries.Where(d => d.Year == year).Select(d => d.DId).ToListAsync())
                .ForEach(a => { if (a <= c + 100) c = a; });
            return c + 1;
        }

        public async Task<int> NextDPNr(int year, int did) {
            int c = 0;
            (await DeliveryParts.Where(p => p.Year == year && p.DId == did).Select(d => d.DPNr).ToListAsync())
                .ForEach(a => { if (a <= c + 100) c = a; });
            return c + 1;
        }

        public async Task<int> NextRdNr(int kgnr) {
            int c = 0;
            (await WbRde.Where(r => r.KgNr == kgnr).Select(r => r.RdNr).ToListAsync())
                .ForEach(a => { if (a <= c + 100) c = a; });
            return c + 1;
        }

        public async Task<int> NextAvNr(int year) {
            int c = 0;
            (await PaymentVariants.Where(v => v.Year == year).Select(v => v.AvNr).ToListAsync())
                .ForEach(a => { if (a <= c + 100) c = a; });
            return c + 1;
        }

        public async Task<WineQualLevel> GetWineQualityLevel(double kmw) {
            return await WineQualityLevels
                .Where(q => !q.IsPredicate && (q.MinKmw == null || q.MinKmw <= kmw))
                .OrderBy(q => q.MinKmw)
                .LastOrDefaultAsync();
        }

        public async Task UpdateDeliveryPartModifiers(DeliveryPart part, IEnumerable<Modifier> modifiers) {
            foreach (var m in Modifiers.Where(m => m.Year == part.Year)) {
                var mod = part.PartModifiers.Where(pa => pa.ModId == m.ModId).FirstOrDefault();
                if (modifiers.Contains(m)) {
                    DeliveryPartModifier dpm = mod ?? this.CreateProxy<DeliveryPartModifier>();
                    dpm.Year = part.Year;
                    dpm.DId = part.DId;
                    dpm.DPNr = part.DPNr;
                    dpm.ModId = m.ModId;
                    if (mod == null) {
                        await AddAsync(dpm);
                    } else {
                        Update(dpm);
                    }
                } else {
                    if (mod != null) {
                        Remove(mod);
                    }
                }
            }
        }

        private async Task FetchMemberAreaCommitmentBuckets(int year, SqliteConnection? cnx = null) {
            var ownCnx = cnx == null;
            cnx ??= await ConnectAsync();
            var buckets = new Dictionary<int, Dictionary<string, AreaComBucket>>();
            using (var cmd = cnx.CreateCommand()) {
                cmd.CommandText = $"SELECT mgnr, bucket, area, min_kg, max_kg FROM v_area_commitment_bucket WHERE year = {year}";
                using var reader = await cmd.ExecuteReaderAsync();
                while (await reader.ReadAsync()) {
                    var mgnr = reader.GetInt32(0);
                    var vtrgid = reader.GetString(1);
                    if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
                    buckets[mgnr][vtrgid] = new(reader.GetInt32(2), reader.GetInt32(3), reader.GetInt32(4));
                }
            }
            if (ownCnx) await cnx.DisposeAsync();
            _memberAreaCommitmentBuckets[year] = buckets;
        }

        private async Task FetchMemberDeliveryBuckets(int year, SqliteConnection? cnx = null) {
            var ownCnx = cnx == null;
            cnx ??= await ConnectAsync();
            var buckets = new Dictionary<int, Dictionary<string, int>>();
            using (var cmd = cnx.CreateCommand()) {
                cmd.CommandText = $"SELECT mgnr, bucket, weight FROM v_delivery_bucket WHERE year = {year}";
                using var reader = await cmd.ExecuteReaderAsync();
                while (await reader.ReadAsync()) {
                    var mgnr = reader.GetInt32(0);
                    var bucket = reader.GetString(1);
                    if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
                    buckets[mgnr][bucket] = reader.GetInt32(2);
                }
            }
            if (ownCnx) await cnx.DisposeAsync();
            _memberDeliveryBuckets[year] = buckets;
        }

        private async Task FetchMemberDeliveryBucketsStrict(int year, SqliteConnection? cnx = null) {
            var ownCnx = cnx == null;
            cnx ??= await ConnectAsync();
            var buckets = new Dictionary<int, Dictionary<string, int>>();
            using (var cmd = cnx.CreateCommand()) {
                cmd.CommandText = $"SELECT mgnr, bucket, weight FROM v_delivery_bucket_strict WHERE year = {year}";
                using var reader = await cmd.ExecuteReaderAsync();
                while (await reader.ReadAsync()) {
                    var mgnr = reader.GetInt32(0);
                    var bucket = reader.GetString(1);
                    if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
                    buckets[mgnr][bucket] = reader.GetInt32(2);
                }
            }
            if (ownCnx) await cnx.DisposeAsync();
            _memberDeliveryBucketsStrict[year] = buckets;
        }

        private async Task FetchMemberPaymentBuckets(int year, SqliteConnection? cnx = null) {
            var ownCnx = cnx == null;
            cnx ??= await ConnectAsync();
            var buckets = new Dictionary<int, Dictionary<string, int>>();
            using (var cmd = cnx.CreateCommand()) {
                cmd.CommandText = $"SELECT mgnr, bucket, weight FROM v_payment_bucket WHERE year = {year}";
                using var reader = await cmd.ExecuteReaderAsync();
                while (await reader.ReadAsync()) {
                    var mgnr = reader.GetInt32(0);
                    var bucket = reader.GetString(1);
                    if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
                    buckets[mgnr][bucket] = reader.GetInt32(2);
                }
            }
            if (ownCnx) await cnx.DisposeAsync();
            _memberPaymentBuckets[year] = buckets;
        }

        private async Task FetchMemberUnderDelivery(int year, SqliteConnection? cnx = null) {
            var ownCnx = cnx == null;
            cnx ??= await ConnectAsync();
            var buckets = new Dictionary<int, Dictionary<string, UnderDelivery>>();
            using (var cmd = cnx.CreateCommand()) {
                cmd.CommandText = $"SELECT mgnr, bucket, weight, diff FROM v_under_delivery WHERE year = {year}";
                using var reader = await cmd.ExecuteReaderAsync();
                while (await reader.ReadAsync()) {
                    var mgnr = reader.GetInt32(0);
                    var bucket = reader.GetString(1);
                    if (!buckets.ContainsKey(mgnr)) buckets[mgnr] = [];
                    buckets[mgnr][bucket] = new(reader.GetInt32(2), reader.GetInt32(3));
                }
            }
            if (ownCnx) await cnx.DisposeAsync();
            _memberUnderDelivery[year] = buckets;
        }

        public async Task<Dictionary<string, AreaComBucket>> GetMemberAreaCommitmentBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
            if (!_memberAreaCommitmentBuckets.ContainsKey(year))
                await FetchMemberAreaCommitmentBuckets(year, cnx);
            return _memberAreaCommitmentBuckets[year].GetValueOrDefault(mgnr, []);
        }

        public async Task<Dictionary<string, int>> GetMemberDeliveryBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
            if (!_memberDeliveryBuckets.ContainsKey(year))
                await FetchMemberDeliveryBuckets(year, cnx);
            return _memberDeliveryBuckets[year].GetValueOrDefault(mgnr, []);
        }

        public async Task<Dictionary<string, int>> GetMemberDeliveryBucketsStrict(int year, int mgnr, SqliteConnection? cnx = null) {
            if (!_memberDeliveryBucketsStrict.ContainsKey(year))
                await FetchMemberDeliveryBucketsStrict(year, cnx);
            return _memberDeliveryBucketsStrict[year].GetValueOrDefault(mgnr, []);
        }

        public async Task<Dictionary<string, int>> GetMemberPaymentBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
            if (!_memberPaymentBuckets.ContainsKey(year))
                await FetchMemberPaymentBuckets(year, cnx);
            return _memberPaymentBuckets[year].GetValueOrDefault(mgnr, []);
        }

        public async Task<Dictionary<string, UnderDelivery>> GetMemberUnderDelivery(int year, int mgnr, SqliteConnection? cnx = null) {
            if (!_memberUnderDelivery.ContainsKey(year))
                await FetchMemberUnderDelivery(year, cnx);
            return _memberUnderDelivery[year].GetValueOrDefault(mgnr, []);
        }

        public async Task<Dictionary<string, MemberBucket>> GetMemberBuckets(int year, int mgnr, SqliteConnection? cnx = null) {
            var ownCnx = cnx == null;
            cnx ??= await ConnectAsync();
            var rightsAndObligations = await GetMemberAreaCommitmentBuckets(year, mgnr, cnx);
            var deliveryBuckets = await GetMemberDeliveryBuckets(year, mgnr, cnx);
            var deliveryBucketsStrict = await GetMemberDeliveryBucketsStrict(year, mgnr, cnx);
            var paymentBuckets = await GetMemberPaymentBuckets(year, mgnr, cnx);
            if (ownCnx) await cnx.DisposeAsync();

            var buckets = new Dictionary<string, MemberBucket>();
            foreach (var id in rightsAndObligations.Keys.Union(deliveryBuckets.Keys).Union(paymentBuckets.Keys)) {
                var variety = await WineVarieties.FindAsync(id[..2]);
                var attribute = await WineAttributes.FindAsync(id[2..]);
                var name = (variety?.Name ?? "") + (id[2..] == "_" ? " (kein Qual.Wein)" : attribute != null ? $" ({attribute})" : "");
                buckets[id] = new(
                    name,
                    rightsAndObligations.GetValueOrDefault(id).Area,
                    rightsAndObligations.GetValueOrDefault(id).Obligation,
                    rightsAndObligations.GetValueOrDefault(id).Right,
                    deliveryBuckets.GetValueOrDefault(id),
                    deliveryBucketsStrict.GetValueOrDefault(id),
                    paymentBuckets.GetValueOrDefault(id)
                );
            }
            return buckets;
        }

        public static async Task<List<MemberStat>> GetMemberStats(int year, int mgnr, SqliteConnection? cnx = null) {
            var ownCnx = cnx == null;
            cnx ??= await ConnectAsync();
            var list = new List<MemberStat>();
            using (var cmd = cnx.CreateCommand()) {
                cmd.CommandText = $"""
                    SELECT v.name AS variety,
                           COALESCE(a.name, '') || IIF(a.name IS NOT NULL AND c.name IS NOT NULL, ' / ', '') || COALESCE(c.name, '') AS disc,
                           SUM(weight) AS weight
                    FROM v_delivery d
                        LEFT JOIN wine_variety v ON v.sortid = d.sortid
                        LEFT JOIN wine_attribute a ON a.attrid = d.attrid
                        LEFT JOIN wine_cultivation c ON c.cultid = d.cultid
                    WHERE d.year = {year} AND d.mgnr = {mgnr}
                    GROUP BY d.sortid, d.attrid, d.cultid
                    ORDER BY variety, disc;
                    """;
                using var reader = await cmd.ExecuteReaderAsync();
                while (await reader.ReadAsync()) {
                    list.Add(new(reader.GetString(0), reader.GetString(1), reader.GetInt32(2)));
                }
            }
            if (ownCnx) await cnx.DisposeAsync();
            return list;
        }
    }
}