using Newtonsoft.Json;
using NJsonSchema;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Text.Json.Nodes;
using System.Threading.Tasks;

namespace Elwig.Helpers.Billing {
    public class BillingData {

        public enum CalculationMode { Elwig, WgMaster }
        public enum CurveMode { Oe, Kmw }
        public record struct Curve(CurveMode Mode, Dictionary<double, decimal> Normal, Dictionary<double, decimal>? Gebunden);

        public static JsonSchema? Schema { get; private set; }

        public static async Task Init() {
            var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream("Elwig.Resources.Schemas.PaymentVariantData.json");
            Schema = await JsonSchema.FromJsonAsync(stream ?? throw new ArgumentException("JSON schema not found"));
        }

        public readonly JsonObject Data;
        public readonly CalculationMode Mode;

        public bool ConsiderDelieryModifiers {
            get => GetConsider("consider_delivery_modifiers");
            set => SetConsider(value, "consider_delivery_modifiers");
        }
        public bool ConsiderContractPenalties {
            get => GetConsider("consider_contract_penalties");
            set => SetConsider(value, "consider_contract_penalties");
        }
        public bool ConsiderTotalPenalty {
            get => GetConsider("consider_total_penalty");
            set => SetConsider(value, "consider_total_penalty");
        }
        public bool ConsiderAutoBusinessShares {
            get => GetConsider("consider_auto_business_shares");
            set => SetConsider(value, "consider_auto_business_shares");
        }
        public bool ConsiderCustomModifiers {
            get => GetConsider("consider_custom_modifiers");
            set => SetConsider(value, "consider_custom_modifiers");
        }

        public double NetWeightModifier {
            get => GetWeightModifier("net_weight_modifier", "Rebelzuschlag");
            set => SetWeightModifier(value, "net_weight_modifier", "Rebelzuschlag");
        }
        public double GrossWeightModifier {
            get => GetWeightModifier("gross_weight_modifier");
            set => SetWeightModifier(value, "gross_weight_modifier");
        }

        private bool GetConsider(string name, string? wgMasterName = null) {
            return ((Mode == CalculationMode.Elwig) ? Data[name] : Data[wgMasterName ?? ""])?.AsValue().GetValue<bool>() ?? false;
        }

        private void SetConsider(bool value, string name, string? wgMasterName = null) {
            if (Mode == CalculationMode.WgMaster && wgMasterName == null) {
                return;
            } else if (value) {
                Data[(Mode == CalculationMode.Elwig) ? name : wgMasterName ?? ""] = value;
            } else {
                Data.Remove((Mode == CalculationMode.Elwig) ? name : wgMasterName ?? "");
            }
        }

        private double GetWeightModifier(string name, string? wgMasterName = null) {
            var isElwig = (Mode == CalculationMode.Elwig);
            var val = (isElwig ? Data[name] : Data[wgMasterName ?? ""])?.AsValue().GetValue<double>() ?? 0;
            return isElwig ? val : val / 100.0;
        }

        private void SetWeightModifier(double value, string name, string? wgMasterName = null) {
            var isElwig = (Mode == CalculationMode.Elwig);
            if (Mode == CalculationMode.WgMaster && wgMasterName == null) {
                return;
            } else if (value != 0) {
                Data[isElwig ? name : wgMasterName ?? ""] = isElwig ? value : value * 100.0;
            } else {
                Data.Remove(isElwig ? name : wgMasterName ?? "");
            }
        }

        public BillingData(JsonObject data) {
            Data = data;
            var mode = Data["mode"]?.GetValue<string>();
            Mode = (mode == "elwig") ? CalculationMode.Elwig : CalculationMode.WgMaster;
        }

        protected static JsonObject ParseJson(string json) {
            if (Schema == null) throw new InvalidOperationException("Schema has to be initialized first");
            try {
                var errors = Schema.Validate(json);
                if (errors.Count != 0) throw new ArgumentException("Invalid JSON data");
                return JsonNode.Parse(json)?.AsObject() ?? throw new ArgumentException("Invalid JSON data");
            } catch (JsonReaderException) {
                throw new ArgumentException("Invalid JSON data");
            }
        }

        public static BillingData FromJson(string json) {
            return new(ParseJson(json));
        }

        protected JsonArray GetCurvesEntry() {
            return Data[Mode == CalculationMode.Elwig ? "curves" : "Kurven"]?.AsArray() ?? throw new InvalidOperationException();
        }

        protected JsonNode GetPaymentEntry() {
            return Data[Mode == CalculationMode.Elwig ? "payment" : "AuszahlungSorten"] ?? throw new InvalidOperationException();
        }

        protected JsonObject? GetQualityEntry() {
            return Data[Mode == CalculationMode.Elwig ? "quality" : "AuszahlungSortenQualitätsstufe"]?.AsObject();
        }

        private static Dictionary<double, decimal> GetCurveData(JsonObject data, CurveMode mode) {
            var dict = new Dictionary<double, decimal>();
            foreach (var (index, price) in data) {
                double idx;
                bool? gtlt = index.StartsWith('>') ? true : index.StartsWith('<') ? false : null;
                if (index.EndsWith("kmw")) {
                    idx = double.Parse(index[(gtlt != null ? 1 : 0)..^3], CultureInfo.InvariantCulture);
                    if (mode == CurveMode.Oe) {
                        idx = Utils.KmwToOe(idx);
                    }
                } else if (index.EndsWith("oe")) {
                    idx = double.Parse(index[(gtlt != null ? 1 : 0)..^2], CultureInfo.InvariantCulture);
                    if (mode == CurveMode.Kmw) {
                        idx = Utils.OeToKmw(idx);
                    }
                } else {
                    throw new InvalidOperationException();
                }
                if (gtlt == true) {
                    idx = Math.BitIncrement(idx);
                } else if (gtlt == false) {
                    idx = Math.BitDecrement(idx);
                }
                dict[idx] = price?.AsValue().GetValue<decimal>() ?? throw new InvalidOperationException();
            }
            return dict;
        }

        protected Dictionary<int, Curve> GetCurves() {
            var dict = new Dictionary<int, Curve>();
            var curves = GetCurvesEntry();
            foreach (var c in curves) {
                var obj = c?.AsObject() ?? throw new InvalidOperationException();
                var id = obj["id"]?.GetValue<int>() ?? throw new InvalidOperationException();
                var cMode = (obj["mode"]?.GetValue<string>() == "kmw") ? CurveMode.Kmw : CurveMode.Oe;
                double quw = cMode == CurveMode.Oe ? 73 : 15;

                Dictionary<double, decimal> c1;
                Dictionary<double, decimal>? c2 = null;
                var norm = obj["data"];
                if (norm is JsonObject) {
                    c1 = GetCurveData(norm.AsObject(), cMode);
                } else if (norm?.AsValue().TryGetValue(out decimal v) == true) {
                    c1 = new() { { quw, v } };
                } else {
                    throw new InvalidOperationException();
                }
                var geb = obj["geb"];
                if (geb is JsonObject) {
                    c2 = GetCurveData(geb.AsObject(), cMode);
                } else if (geb?.AsValue().TryGetValue(out decimal v) == true) {
                    var splitVal = GetCurveValueAt(c1, quw);
                    c2 = c1.ToDictionary(e => e.Key, e => e.Value + (e.Key >= quw ? v : 0));
                    c2[quw] = splitVal + v;
                    c2[Math.BitDecrement(quw)] = splitVal;
                }
                dict.Add(id, new(cMode, c1, c2));
            }
            return dict;
        }

        protected static Dictionary<RawVaribute, JsonValue> GetSelection(JsonNode value, IEnumerable<RawVaribute> vaributes) {
            if (value is JsonValue flatRate) {
                return vaributes.ToDictionary(e => e, _ => flatRate);
            } if (value is not JsonObject data) {
                throw new InvalidOperationException();
            }
            Dictionary<RawVaribute, JsonValue> dict;
            if (data["default"] is JsonValue def) {
                dict = vaributes.ToDictionary(e => e, _ => def);
            } else {
                dict = [];
            }

            var conv = data
                .Where(p => p.Key != "default")
                .Select(p => (new RawVaribute(p.Key), p.Value))
                .OrderBy(p => (p.Item1.SortId != null ? 10 : 0) + (p.Item1.AttrId != null ? 12 : 0) + (p.Item1.CultId != null ? 11 : 0))
                .ToList();
            foreach (var (idx, v) in conv) {
                var curve = v?.AsValue() ?? throw new InvalidOperationException();
                foreach (var i in vaributes.Where(e =>
                        (idx.SortId == null || idx.SortId == e.SortId) &&
                        (idx.AttrId == null || idx.AttrId == e.AttrId) &&
                        (idx.CultId == null || idx.CultId == e.CultId))) {
                    dict[i] = curve;
                }
            }

            return dict;
        }

        public static decimal GetCurveValueAt(Dictionary<double, decimal> curve, double key) {
            if (curve.Count == 1) return curve.First().Value;

            var lt = curve.Keys.Where(v => v <= key);
            var gt = curve.Keys.Where(v => v >= key);
            if (!lt.Any()) {
                return curve[gt.Min()];
            } else if (!gt.Any()) {
                return curve[lt.Max()];
            }

            var max = lt.Max();
            var min = gt.Min();
            if (max == min) return curve[key];

            var p1 = ((decimal)key - (decimal)min) / ((decimal)max - (decimal)min);
            var p2 = 1 - p1;
            return curve[min] * p2 + curve[max] * p1;
        }

        protected static JsonObject GraphToJson(Graph graph, string mode) {
            var x = graph.DataX;
            var y = graph.DataY;
            var prec = graph.Precision;

            try {
                return new JsonObject() {
                    ["15kmw"] = Math.Round(y.Distinct().Single(), prec)
                };
            } catch { }

            var data = new JsonObject();
            if (y[0] != y[1]) {
                data[$"{x[0]}{mode}"] = Math.Round(y[0], prec);
            }
            for (int i = 1; i < x.Length - 1; i++) {
                var d1 = Math.Round(y[i] - y[i - 1], prec);
                var d2 = Math.Round(y[i + 1] - y[i], prec);
                if (d1 != d2) {
                    data[$"{x[i]}{mode}"] = Math.Round(y[i], prec);
                }
            }
            if (y[^1] != y[^2]) {
                data[$"{x[^1]}{mode}"] = Math.Round(y[^1], prec);
            }
            return data;
        }

        protected static JsonNode GraphEntryToJson(GraphEntry entry) {
            try {
                if (entry.GebundenFlatBonus == null) {
                    return JsonValue.Create((decimal)entry.DataGraph.DataY.Distinct().Single());
                }
            } catch { }

            var curve = new JsonObject {
                ["id"] = entry.Id,
                ["mode"] = entry.Mode.ToString().ToLower(),
            };

            curve["data"] = GraphToJson(entry.DataGraph, entry.Mode.ToString().ToLower());

            if (entry.GebundenFlatBonus != null) {
                curve["geb"] = (decimal)entry.GebundenFlatBonus;
            } else if (entry.GebundenGraph != null) {
                curve["geb"] = GraphToJson(entry.GebundenGraph, entry.Mode.ToString().ToLower());
            }

            return curve;
        }

        protected static (Dictionary<string, List<string>>, Dictionary<decimal, List<string>>) GetReverseKeys(JsonObject data, bool strict = true) {
            Dictionary<string, List<string>> rev1 = [];
            Dictionary<decimal, List<string>> rev2 = [];
            foreach (var (k, v) in data) {
                if (k == "default" || (strict && (k.StartsWith('/') || !k.Contains('/'))) || v is not JsonValue val) {
                    continue;
                } else if (val.TryGetValue<decimal>(out var dec)) {
                    rev2[dec] = rev2.GetValueOrDefault(dec) ?? [];
                    rev2[dec].Add(k);
                } else if (val.TryGetValue<string>(out var cur)) {
                    rev1[cur] = rev1.GetValueOrDefault(cur) ?? [];
                    rev1[cur].Add(k);
                }
            }
            return (rev1, rev2);
        }

        protected static void CollapsePaymentData(JsonObject data, JsonObject originalData, IEnumerable<RawVaribute> vaributes, bool useDefault = true) {
            var (rev1, rev2) = GetReverseKeys(data);
            if (!data.ContainsKey("default")) {
                foreach (var (v, ks) in rev1) {
                    if ((ks.Count > vaributes.Count() * 0.5 && useDefault) || ks.Count == vaributes.Count()) {
                        foreach (var k in ks) {
                            if (!(originalData[$"{k[..2]}/"]?.AsValue().TryGetValue<string>(out var o) ?? false) || o == v)
                                if (!(originalData[k.Split('-')[0]]?.AsValue().TryGetValue<string>(out var o2) ?? false) || o2 == v)
                                    data.Remove(k);
                        }
                        data["default"] = v;
                        CollapsePaymentData(data, originalData, vaributes, useDefault);
                        return;
                    }
                }
                foreach (var (v, ks) in rev2) {
                    if ((ks.Count > vaributes.Count() * 0.5 && useDefault) || ks.Count == vaributes.Count()) {
                        foreach (var k in ks) {
                            if (!(originalData[$"{k[..2]}/"]?.AsValue().TryGetValue<decimal>(out var o) ?? false) || o == v)
                                if (!(originalData[k.Split('-')[0]]?.AsValue().TryGetValue<decimal>(out var o2) ?? false) || o2 == v)
                                    data.Remove(k);
                        }
                        data["default"] = v;
                        CollapsePaymentData(data, originalData, vaributes, useDefault);
                        return;
                    }
                }
            }

            var attributes = data
                .Select(e => e.Key)
                .Where(k => k.Length > 3 && k.Contains('/'))
                .Select(k => k.Split('/')[1])
                .Distinct()
                .ToList();
            foreach (var idx in attributes) {
                var len = vaributes.Count(e => $"{e.AttrId}{(e.CultId != null && e.CultId != "" ? "-" : "")}{e.CultId}" == idx);
                foreach (var (v, ks) in rev1) {
                    var myKs = ks.Where(k => k.EndsWith($"/{idx}")).ToList();
                    if (myKs.Count > 1 && ((myKs.Count > len * 0.5 && useDefault) || myKs.Count == len)) {
                        foreach (var k in myKs) data.Remove(k);
                        var discr = (idx.StartsWith('-') && !useDefault ? "" : "/") + idx;
                        data[discr] = v;
                        foreach (var (k, o) in originalData) {
                            if (o!.AsValue().TryGetValue<string>(out var o2) && o2 != v && k.Contains(discr))
                                data[k] = o2;
                        }
                    }
                }
                foreach (var (v, ks) in rev2) {
                    var myKs = ks.Where(k => k.EndsWith($"/{idx}")).ToList();
                    if (myKs.Count > 1 && ((myKs.Count > len * 0.5 && useDefault) || myKs.Count == len)) {
                        foreach (var k in myKs) data.Remove(k);
                        var discr = (idx.StartsWith('-') && !useDefault ? "" : "/") + idx;
                        data[discr] = v;
                        foreach (var (k, o) in originalData) {
                            if (o!.AsValue().TryGetValue<decimal>(out var o2) && o2 != v && k.Contains(discr))
                                data[k] = o2;
                        }
                    }
                }
            }

            if (!useDefault)
                return;

            var keys = data.Select(p => p.Key).ToList();
            foreach (var k in keys) {
                if (k.Length == 3 && k.EndsWith('/') && !keys.Contains(k[..2])) {
                    data.Remove(k, out var val);
                    data.Add(k[..2], val);
                } else if (k.Contains("/-")) {
                    data.Remove(k, out var val);
                    data.Add(k.Replace("/-", "-"), val);
                    if (k[0] == '/' || k.Contains('-')) {
                        foreach (var (k2, o) in originalData) {
                            if (!data.ContainsKey(k2) && k2.Contains('-') && k2.Contains("-" + k.Split('-')[1]) && !k2.Contains("/-")
                                && (!k2.Contains('/') || k2.Length <= 4 || !data.ContainsKey(k2[2..])))
                            {
                                data[k2] = o?.DeepClone();
                            }
                        }
                    }
                }
            }

            (rev1, rev2) = GetReverseKeys(data, false);
            var keyVaributes = data
                .Select(e => e.Key)
                .Where(e => e.Length > 0 && !e.Contains('-') && e != "default")
                .Distinct()
                .ToList();
            foreach (var idx in keyVaributes) {
                var len = data.Count(e => e.Key == idx || (e.Key.Length > idx.Length && e.Key.StartsWith(idx) && e.Key[idx.Length] == '-'));
                foreach (var (v, ks) in rev1) {
                    var myKs = ks.Where(k => k == idx || (k.Length > idx.Length && k.StartsWith(idx) && k[idx.Length] == '-' && !data.ContainsKey(k[idx.Length..]))).ToList();
                    if (myKs.Count == len) {
                        foreach (var k in myKs) {
                            if (k != idx) data.Remove(k);
                        }
                    }
                }
                foreach (var (v, ks) in rev2) {
                    var myKs = ks.Where(k => k == idx || (k.Length > idx.Length && k.StartsWith(idx) && k[idx.Length] == '-' && !data.ContainsKey(k[idx.Length..]))).ToList();
                    if (myKs.Count == len) {
                        foreach (var k in myKs) {
                            if (k != idx) data.Remove(k);
                        }
                    }
                }
            }
        }

        public static JsonObject FromGraphEntries(
            IEnumerable<GraphEntry> graphEntries,
            BillingData? origData = null,
            IEnumerable<RawVaribute>? vaributes = null,
            bool useDefaultPayment = true,
            bool useDefaultQuality = true
        ) {
            var payment = new JsonObject();
            var qualityWei = new JsonObject();
            var curves = new JsonArray();
            int curveId = 0;
            foreach (var entry in graphEntries) {
                var curve = GraphEntryToJson(entry);
                JsonValue node;
                if (curve is JsonObject obj) {
                    obj["id"] = ++curveId;
                    node = JsonValue.Create($"curve:{curveId}");
                    curves.Add(obj);
                } else if (curve is JsonValue val && val.TryGetValue<decimal>(out var flat)) {
                    node = JsonValue.Create(flat);
                } else {
                    continue;
                }
                foreach (var c in entry.Vaributes) {
                    var v = new RawVaribute(c.Variety!.SortId, c.Attribute?.AttrId ?? "", c.Cultivation?.CultId);
                    if (v.CultId == "") v.CultId = null;
                    if (entry.Abgewertet) {;
                        qualityWei[v.ToString()] = node.DeepClone();
                    } else {
                        payment[v.ToString()] = node.DeepClone();
                    }
                }
            }

            CollapsePaymentData(payment, payment.DeepClone().AsObject(), vaributes ?? payment.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultPayment);
            CollapsePaymentData(qualityWei, qualityWei.DeepClone().AsObject(), vaributes ?? qualityWei.Select(e => new RawVaribute(e.Key)).ToList(), useDefaultQuality);

            var data = new JsonObject {
                ["mode"] = "elwig",
                ["version"] = 1,
            };

            if (origData?.ConsiderDelieryModifiers == true)
                data["consider_delivery_modifiers"] = true;
            if (origData?.ConsiderContractPenalties == true)
                data["consider_contract_penalties"] = true;
            if (origData?.ConsiderTotalPenalty == true)
                data["consider_total_penalty"] = true;
            if (origData?.ConsiderAutoBusinessShares == true)
                data["consider_auto_business_shares"] = true;

            if (payment.Count == 0) {
                data["payment"] = 0;
            } else if (payment.Count == 1 && payment.First().Key == "default") {
                data["payment"] = payment.Single().Value?.DeepClone();
            } else {
                data["payment"] = payment;
            }
            if (qualityWei.Count == 1 && qualityWei.First().Key == "default") {
                data["quality"] = new JsonObject() {
                    ["WEI"] = qualityWei.Single().Value?.DeepClone()
                };
            } else if (qualityWei.Count >= 1) {
                data["quality"] = new JsonObject() {
                    ["WEI"] = qualityWei
                };
            }
            data["curves"] = curves;

            return data;
        }
    }
}
