using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json.Linq;

namespace ZB.MOM.WW.GRAccess.Cli.GRAccess
{
    public sealed class PackageAttributeValue
    {
        public string Name { get; set; } = string.Empty;
        public string DataType { get; set; } = string.Empty;
        public object Value { get; set; }
        public string Source { get; set; } = "package";
    }

    public sealed class PackageScriptBody
    {
        public string Name { get; set; } = string.Empty;
        public string Body { get; set; } = string.Empty;
        public string Source { get; set; } = "package";
    }

    public sealed class PackageSnapshot
    {
        public List<string> Lineage { get; } = new List<string>();
        public List<GRAccessObjectInfo> Children { get; } = new List<GRAccessObjectInfo>();
        public List<GRAccessObjectInfo> ContainedObjects { get; } = new List<GRAccessObjectInfo>();
        public List<PackageAttributeValue> AttributeValues { get; } = new List<PackageAttributeValue>();
        public List<PackageScriptBody> ScriptBodies { get; } = new List<PackageScriptBody>();
        public List<LlmUnavailableField> Unavailable { get; } = new List<LlmUnavailableField>();
        public bool PackageFallbackUsed { get; set; }
        public string Source { get; set; } = "direct-graccess";
    }

    public static class PackageSnapshotParser
    {
        private static readonly Regex LineageRegex = new Regex(@"Lineage\s*[:=]\s*(?<value>.+)$", RegexOptions.IgnoreCase | RegexOptions.Multiline | RegexOptions.Compiled);
        private static readonly Regex DerivedRegex = new Regex(@"DerivedFrom\s*[:=]\s*(?<value>[^\r\n<]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private static readonly Regex BasedOnRegex = new Regex(@"BasedOn\s*[:=]\s*(?<value>[^\r\n<]+)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private static readonly Regex XmlValueRegex = new Regex(@"<(?<tag>DerivedFrom|BasedOn)>\s*(?<value>[^<]+)\s*</\k<tag>>", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private static readonly Regex AttributeRegex = new Regex(@"AttributeValue\s+Name=""(?<name>[^""]+)""(?:\s+DataType=""(?<type>[^""]*)"")?\s+Value=""(?<value>[^""]*)""", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private static readonly Regex AttributeKeyRegex = new Regex(@"AttributeValues\.(?<name>[^=\r\n]+)\s*=\s*(?<value>[^\r\n]*)", RegexOptions.IgnoreCase | RegexOptions.Compiled);
        private static readonly Regex ScriptRegex = new Regex(@"ScriptBody\s+Name=""(?<name>[^""]+)""\s*>\s*(?<body>.*?)\s*</ScriptBody>", RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.Compiled);
        private static readonly Regex ScriptKeyRegex = new Regex(@"ScriptBodies\.(?<name>[^=\r\n]+)\s*=\s*(?<value>[\s\S]*?)(?=\r?\n[A-Za-z0-9_.-]+\s*=|\z)", RegexOptions.IgnoreCase | RegexOptions.Compiled);

        public static PackageSnapshot Parse(string path)
        {
            var snapshot = new PackageSnapshot { PackageFallbackUsed = true, Source = "export-package" };
            var entries = ReadTextEntries(path).ToList();
            if (!entries.Any() && File.Exists(path))
            {
                try
                {
                    entries.Add(File.ReadAllText(path));
                }
                catch
                {
                    snapshot.Unavailable.Add(new LlmUnavailableField { Field = "package", Reason = "Package file could not be read as text." });
                }
            }

            foreach (var text in entries)
                ParseText(text, snapshot);

            if (File.Exists(path) && !LooksLikeZip(path))
            {
                try
                {
                    var text = File.ReadAllText(path);
                    ParseText(text, snapshot);
                    ParseLineageLines(text, snapshot);
                }
                catch
                {
                    // Directory and archive package paths are handled above.
                }
            }

            Deduplicate(snapshot);
            return snapshot;
        }

        private static IEnumerable<string> ReadTextEntries(string path)
        {
            if (Directory.Exists(path))
            {
                foreach (var file in Directory.EnumerateFiles(path, "*.*", SearchOption.AllDirectories))
                    foreach (var text in ReadPackageFile(file, 0))
                        yield return text;
                yield break;
            }

            if (!File.Exists(path))
                yield break;

            foreach (var text in ReadPackageFile(path, 0))
                yield return text;
        }

        private static IEnumerable<string> ReadPackageFile(string path, int depth)
        {
            byte[] bytes;
            try
            {
                bytes = File.ReadAllBytes(path);
            }
            catch
            {
                yield break;
            }

            foreach (var text in ReadPackageBytes(bytes, path, depth))
                yield return text;
        }

        private static IEnumerable<string> ReadPackageBytes(byte[] bytes, string name, int depth)
        {
            if (bytes == null || bytes.Length == 0 || depth > 6)
                yield break;

            if (LooksLikeZip(bytes))
            {
                using (var stream = new MemoryStream(bytes))
                using (var archive = new ZipArchive(stream, ZipArchiveMode.Read))
                {
                    foreach (var entry in archive.Entries)
                    {
                        if (entry.Length == 0)
                            continue;

                        byte[] entryBytes;
                        using (var entryStream = entry.Open())
                        using (var copy = new MemoryStream())
                        {
                            entryStream.CopyTo(copy);
                            entryBytes = copy.ToArray();
                        }

                        foreach (var text in ReadPackageBytes(entryBytes, $"{name}!{entry.FullName}", depth + 1))
                            yield return text;
                    }
                }
                yield break;
            }

            if (LooksTextual(name))
            {
                var utf8 = ReadText(bytes, Encoding.UTF8);
                if (!string.IsNullOrWhiteSpace(utf8))
                    yield return utf8;
            }

            var utf16Strings = ExtractUtf16Strings(bytes);
            if (!string.IsNullOrWhiteSpace(utf16Strings))
                yield return utf16Strings;
        }

        private static IEnumerable<string> ReadTextFile(string path)
        {
            string text;
            try
            {
                text = File.ReadAllText(path);
            }
            catch
            {
                yield break;
            }

            if (!string.IsNullOrWhiteSpace(text))
                yield return text;
        }

        private static bool LooksLikeZip(string path)
        {
            try
            {
                using (var stream = File.OpenRead(path))
                {
                    if (stream.Length < 4)
                        return false;

                    return stream.ReadByte() == 0x50 && stream.ReadByte() == 0x4b;
                }
            }
            catch
            {
                return false;
            }
        }

        private static bool LooksLikeZip(byte[] bytes)
        {
            return bytes.Length >= 4 && bytes[0] == 0x50 && bytes[1] == 0x4b;
        }

        private static bool LooksTextual(string path)
        {
            var ext = Path.GetExtension(path).ToLowerInvariant();
            return ext == ".xml" || ext == ".json" || ext == ".txt" || ext == ".csv" || ext == ".ini" || ext == ".config" || ext == ".pkg" || ext == ".aapkg";
        }

        private static string ReadText(byte[] bytes, Encoding fallback)
        {
            try
            {
                using (var stream = new MemoryStream(bytes))
                using (var reader = new StreamReader(stream, fallback, true))
                    return reader.ReadToEnd();
            }
            catch
            {
                return string.Empty;
            }
        }

        private static string ExtractUtf16Strings(byte[] bytes)
        {
            var lines = new List<string>();
            var builder = new StringBuilder();
            for (var i = 0; i < bytes.Length - 1;)
            {
                var start = i;
                builder.Clear();
                while (i < bytes.Length - 1)
                {
                    var lo = bytes[i];
                    var hi = bytes[i + 1];
                    if (hi == 0 && IsPackageTextByte(lo))
                    {
                        builder.Append((char)lo);
                        i += 2;
                    }
                    else
                    {
                        break;
                    }
                }

                if (builder.Length >= 4)
                    lines.Add(builder.ToString());

                if (i == start)
                    i++;
            }

            return string.Join("\n", lines);
        }

        private static bool IsPackageTextByte(byte value)
        {
            return (value >= 32 && value <= 126) || value == 9 || value == 10 || value == 13;
        }

        private static void ParseText(string text, PackageSnapshot snapshot)
        {
            TryParseJson(text, snapshot);
            ParseLineage(text, snapshot);
            ParseAttributes(text, snapshot);
            ParseScripts(text, snapshot);
        }

        private static void TryParseJson(string text, PackageSnapshot snapshot)
        {
            try
            {
                var token = JToken.Parse(text);
                foreach (var item in token.SelectTokens("$..Lineage[*]").Values<string>())
                    AddLineage(snapshot, item);
                foreach (var item in token.SelectTokens("$..Children[*]"))
                    AddObject(snapshot.Children, item);
                foreach (var item in token.SelectTokens("$..ContainedObjects[*]"))
                    AddObject(snapshot.ContainedObjects, item);
                foreach (var item in token.SelectTokens("$..AttributeValues[*]"))
                    AddAttribute(snapshot, item);
                foreach (var item in token.SelectTokens("$..ScriptBodies[*]"))
                    AddScript(snapshot, item);
            }
            catch
            {
                // Non-JSON package entries are parsed by the text matchers.
            }
        }

        private static void ParseLineage(string text, PackageSnapshot snapshot)
        {
            ParseLineageLines(text, snapshot);

            foreach (Match match in LineageRegex.Matches(text))
                foreach (var item in match.Groups["value"].Value.Split(new[] { "->", "|", "," }, StringSplitOptions.RemoveEmptyEntries))
                    AddLineage(snapshot, item);

            foreach (Match match in DerivedRegex.Matches(text))
                AddLineage(snapshot, match.Groups["value"].Value);
            foreach (Match match in BasedOnRegex.Matches(text))
                AddLineage(snapshot, match.Groups["value"].Value);
            foreach (Match match in XmlValueRegex.Matches(text))
                AddLineage(snapshot, match.Groups["value"].Value);
        }

        private static void ParseLineageLines(string text, PackageSnapshot snapshot)
        {
            foreach (var line in text.Split(new[] { "\r\n", "\n" }, StringSplitOptions.None))
            {
                var trimmed = line.Trim();
                if (!trimmed.StartsWith("Lineage:", StringComparison.OrdinalIgnoreCase) && !trimmed.StartsWith("Lineage=", StringComparison.OrdinalIgnoreCase))
                    continue;

                var separator = trimmed.IndexOfAny(new[] { ':', '=' });
                if (separator < 0)
                    continue;

                var value = trimmed.Substring(separator + 1);
                foreach (var item in value.Split(new[] { "->", "|", "," }, StringSplitOptions.RemoveEmptyEntries))
                    AddLineage(snapshot, item);
            }
        }

        private static void ParseAttributes(string text, PackageSnapshot snapshot)
        {
            foreach (Match match in AttributeRegex.Matches(text))
            {
                snapshot.AttributeValues.Add(new PackageAttributeValue
                {
                    Name = match.Groups["name"].Value.Trim(),
                    DataType = match.Groups["type"].Value.Trim(),
                    Value = match.Groups["value"].Value
                });
            }

            foreach (Match match in AttributeKeyRegex.Matches(text))
            {
                snapshot.AttributeValues.Add(new PackageAttributeValue
                {
                    Name = match.Groups["name"].Value.Trim(),
                    Value = match.Groups["value"].Value.Trim()
                });
            }
        }

        private static void ParseScripts(string text, PackageSnapshot snapshot)
        {
            foreach (Match match in ScriptRegex.Matches(text))
                snapshot.ScriptBodies.Add(new PackageScriptBody { Name = match.Groups["name"].Value.Trim(), Body = match.Groups["body"].Value });

            foreach (Match match in ScriptKeyRegex.Matches(text))
                snapshot.ScriptBodies.Add(new PackageScriptBody { Name = match.Groups["name"].Value.Trim(), Body = match.Groups["value"].Value.Trim() });

            ParseBinaryScriptRecords(text, snapshot);
        }

        private static void ParseBinaryScriptRecords(string text, PackageSnapshot snapshot)
        {
            var lines = text
                .Split(new[] { "\r\n", "\n" }, StringSplitOptions.None)
                .Select(line => line.Trim())
                .Where(line => !string.IsNullOrWhiteSpace(line))
                .ToList();

            for (var i = 0; i < lines.Count; i++)
            {
                var marker = lines[i];
                if (!marker.EndsWith("_ScriptExtension", StringComparison.OrdinalIgnoreCase))
                    continue;

                var baseName = marker.Substring(0, marker.Length - "_ScriptExtension".Length);
                if (string.IsNullOrWhiteSpace(baseName))
                    continue;

                for (var j = i + 1; j < Math.Min(lines.Count, i + 12); j++)
                {
                    var body = lines[j];
                    if (!LooksLikeScriptBody(body))
                        continue;

                    AddScriptBody(snapshot, $"{baseName}.ExecuteText", body, "export-package:binary");
                    break;
                }
            }
        }

        private static bool LooksLikeScriptBody(string value)
        {
            if (string.IsNullOrWhiteSpace(value))
                return false;

            var trimmed = value.Trim();
            if (trimmed.StartsWith("{", StringComparison.Ordinal) && trimmed.EndsWith("}", StringComparison.Ordinal))
                return false;
            if (trimmed.IndexOf("_ScriptExtension", StringComparison.OrdinalIgnoreCase) >= 0)
                return false;

            return trimmed.IndexOf("Me.", StringComparison.OrdinalIgnoreCase) >= 0
                || trimmed.IndexOf("System.", StringComparison.OrdinalIgnoreCase) >= 0
                || trimmed.IndexOf("ScriptExchange", StringComparison.OrdinalIgnoreCase) >= 0
                || trimmed.IndexOf(";", StringComparison.OrdinalIgnoreCase) >= 0
                || trimmed.IndexOf(":=", StringComparison.OrdinalIgnoreCase) >= 0;
        }

        private static void AddScriptBody(PackageSnapshot snapshot, string name, string body, string source)
        {
            snapshot.ScriptBodies.Add(new PackageScriptBody { Name = name, Body = body, Source = source });
            snapshot.AttributeValues.Add(new PackageAttributeValue
            {
                Name = name,
                DataType = "MxBigString",
                Value = body,
                Source = source
            });
        }

        private static void AddObject(List<GRAccessObjectInfo> target, JToken token)
        {
            target.Add(new GRAccessObjectInfo
            {
                Kind = Value(token, "Kind"),
                Tagname = Value(token, "Tagname", "Name"),
                ContainedName = Value(token, "ContainedName"),
                HierarchicalName = Value(token, "HierarchicalName"),
                CheckoutStatus = Value(token, "CheckoutStatus"),
                DerivedFrom = Value(token, "DerivedFrom"),
                BasedOn = Value(token, "BasedOn"),
                Container = Value(token, "Container")
            });
        }

        private static void AddAttribute(PackageSnapshot snapshot, JToken token)
        {
            snapshot.AttributeValues.Add(new PackageAttributeValue
            {
                Name = Value(token, "Name", "Attribute"),
                DataType = Value(token, "DataType"),
                Value = token["Value"]?.ToObject<object>()
            });
        }

        private static void AddScript(PackageSnapshot snapshot, JToken token)
        {
            snapshot.ScriptBodies.Add(new PackageScriptBody
            {
                Name = Value(token, "Name", "Script"),
                Body = Value(token, "Body", "Text")
            });
        }

        private static string Value(JToken token, params string[] names)
        {
            foreach (var name in names)
            {
                var value = token[name] ?? token[name.ToLowerInvariant()];
                if (value != null)
                    return value.Type == JTokenType.Null ? string.Empty : value.ToString();
            }

            return string.Empty;
        }

        private static void AddLineage(PackageSnapshot snapshot, string value)
        {
            value = (value ?? string.Empty).Trim();
            if (!string.IsNullOrWhiteSpace(value))
                snapshot.Lineage.Add(value);
        }

        private static void Deduplicate(PackageSnapshot snapshot)
        {
            Replace(snapshot.Lineage, snapshot.Lineage.Where(s => !string.IsNullOrWhiteSpace(s)).Distinct(StringComparer.OrdinalIgnoreCase));
            Replace(snapshot.Children, snapshot.Children.GroupBy(o => $"{o.Kind}:{o.Tagname}", StringComparer.OrdinalIgnoreCase).Select(g => g.First()));
            Replace(snapshot.ContainedObjects, snapshot.ContainedObjects.GroupBy(o => $"{o.Kind}:{o.Tagname}", StringComparer.OrdinalIgnoreCase).Select(g => g.First()));
            Replace(snapshot.AttributeValues, snapshot.AttributeValues.Where(a => !string.IsNullOrWhiteSpace(a.Name)).GroupBy(a => a.Name, StringComparer.OrdinalIgnoreCase).Select(g => g.First()));
            Replace(snapshot.ScriptBodies, snapshot.ScriptBodies.Where(s => !string.IsNullOrWhiteSpace(s.Name)).GroupBy(s => s.Name, StringComparer.OrdinalIgnoreCase).Select(g => g.First()));
        }

        private static void Replace<T>(List<T> list, IEnumerable<T> values)
        {
            var materialized = values.ToList();
            list.Clear();
            list.AddRange(materialized);
        }
    }
}
