using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;

namespace Davanci.Nexus.Editor
{
    public enum RemoteKeyKind { String, Bool, Int, Float, Variant, Presence }

    public struct RemoteKeyCallSite
    {
        public string AssetPath;
        public int Line;
    }

    public sealed class RemoteKeyUsage
    {
        public string Key;
        public bool IsExperiment;
        public string Default;                  // source text of the fallback, null when omitted
        public readonly List<RemoteKeyCallSite> Sites = new List<RemoteKeyCallSite>();

        readonly List<RemoteKeyKind> _kinds = new List<RemoteKeyKind>();

        public IReadOnlyList<RemoteKeyKind> Kinds => _kinds;

        // HasKey says nothing about the value's type, so it never wins the label.
        public RemoteKeyKind Kind
        {
            get
            {
                foreach (var k in _kinds)
                    if (k != RemoteKeyKind.Presence) return k;
                return _kinds.Count > 0 ? _kinds[0] : RemoteKeyKind.String;
            }
        }

        // The same key read as two different types: one of the call sites will silently
        // fall back forever, because a payload can only carry one parseable form.
        public bool Conflicted
        {
            get
            {
                int typed = 0;
                foreach (var k in _kinds)
                    if (k != RemoteKeyKind.Presence && k != RemoteKeyKind.Variant) typed++;
                return typed > 1;
            }
        }

        public void AddKind(RemoteKeyKind kind)
        {
            if (!_kinds.Contains(kind)) _kinds.Add(kind);
        }
    }

    // Greps the project's scripts for the remote-config getters, so the wizard can show
    // which keys a game actually reads and the dashboard can offer exactly those. This is
    // the bridge between the package and the platform. Package scripts are excluded.
    public static class NexusRemoteConfigUsageScanner
    {
        static readonly Regex CallPattern = new Regex(
            @"\b(?:DavanciNexus|NexusRemoteConfig)\.(GetString|GetBool|GetInt|GetFloat|GetVariant|HasKey)\s*\(",
            RegexOptions.Compiled);

        public static List<RemoteKeyUsage> Scan()
        {
            var byId = new Dictionary<string, RemoteKeyUsage>(StringComparer.Ordinal);
            var ordered = new List<RemoteKeyUsage>();

            foreach (var file in NexusEventUsageScanner.EnumerateGameScripts())
            {
                string text;
                try { text = File.ReadAllText(file); }
                catch (IOException) { continue; }

                string assetPath = NexusEventUsageScanner.ToAssetPath(file);

                foreach (Match match in CallPattern.Matches(text))
                {
                    if (IsCommentedOut(text, match.Index)) continue;

                    var args = ReadArguments(text, match.Index + match.Length);
                    if (args == null || args.Count == 0) continue;

                    string key = Unquote(args[0]);
                    if (key == null) continue;          // computed key, nothing to offer the dashboard

                    var kind = KindOf(match.Groups[1].Value);
                    bool experiment = kind == RemoteKeyKind.Variant;

                    // An experiment and a value key may legitimately share a name.
                    string id = (experiment ? "x:" : "v:") + key;
                    if (!byId.TryGetValue(id, out var usage))
                    {
                        usage = new RemoteKeyUsage { Key = key, IsExperiment = experiment };
                        byId[id] = usage;
                        ordered.Add(usage);
                    }

                    usage.AddKind(kind);
                    if (usage.Default == null && args.Count > 1)
                        usage.Default = args[1].Trim();

                    usage.Sites.Add(new RemoteKeyCallSite { AssetPath = assetPath, Line = LineOf(text, match.Index) });
                }
            }

            ordered.Sort((a, b) =>
            {
                int byKind = a.IsExperiment.CompareTo(b.IsExperiment);
                return byKind != 0 ? byKind : string.CompareOrdinal(a.Key, b.Key);
            });
            return ordered;
        }

        // Handed to the dashboard so its key editor offers exactly what this build reads,
        // with the shipped default pre-filled. Values are strings here for the same reason
        // they are strings on the wire.
        public static string ToDashboardJson(List<RemoteKeyUsage> usages)
        {
            var cfg = NexusConfig.Load();
            var sb = new StringBuilder();

            sb.Append("{\n  \"gameId\": ").Append(Quote(cfg == null ? "" : (cfg.gameId ?? "").Trim()));
            sb.Append(",\n  \"generatedAt\": ").Append(Quote(DateTime.UtcNow.ToString("o")));

            sb.Append(",\n  \"keys\": [");
            bool first = true;
            foreach (var u in usages)
            {
                if (u.IsExperiment) continue;
                sb.Append(first ? "\n" : ",\n");
                first = false;
                sb.Append("    { \"key\": ").Append(Quote(u.Key))
                  .Append(", \"type\": ").Append(Quote(TypeName(u.Kind)))
                  .Append(", \"default\": ").Append(Quote(NormalizeDefault(u.Default)))
                  .Append(", \"sites\": ").Append(u.Sites.Count)
                  .Append(" }");
            }
            sb.Append(first ? "]" : "\n  ]");

            sb.Append(",\n  \"experiments\": [");
            first = true;
            foreach (var u in usages)
            {
                if (!u.IsExperiment) continue;
                sb.Append(first ? "\n" : ",\n");
                first = false;
                sb.Append("    { \"key\": ").Append(Quote(u.Key))
                  .Append(", \"sites\": ").Append(u.Sites.Count)
                  .Append(" }");
            }
            sb.Append(first ? "]" : "\n  ]");

            sb.Append("\n}");
            return sb.ToString();
        }

        public static string TypeName(RemoteKeyKind kind)
        {
            switch (kind)
            {
                case RemoteKeyKind.Bool: return "bool";
                case RemoteKeyKind.Int: return "int";
                case RemoteKeyKind.Float: return "float";
                case RemoteKeyKind.Variant: return "variant";
                case RemoteKeyKind.Presence: return "unknown";
                default: return "string";
            }
        }

        static RemoteKeyKind KindOf(string method)
        {
            switch (method)
            {
                case "GetBool": return RemoteKeyKind.Bool;
                case "GetInt": return RemoteKeyKind.Int;
                case "GetFloat": return RemoteKeyKind.Float;
                case "GetVariant": return RemoteKeyKind.Variant;
                case "HasKey": return RemoteKeyKind.Presence;
                default: return RemoteKeyKind.String;
            }
        }

        // "1.5f" and "\"gold\"" are C# source, the dashboard wants the wire form. A default
        // that isn't a literal (Mathf.Max(1, 3), a const, a field) has no value to publish,
        // so it comes back empty rather than as source the dashboard would pre-fill.
        static readonly Regex NumberLiteral = new Regex(@"^[+-]?(\d+\.?\d*|\.\d+)[fFdDmM]?$", RegexOptions.Compiled);

        static string NormalizeDefault(string raw)
        {
            if (string.IsNullOrEmpty(raw)) return "";

            string unquoted = Unquote(raw);
            if (unquoted != null) return unquoted;

            raw = raw.Trim();
            if (raw == "true" || raw == "false") return raw;

            if (!NumberLiteral.IsMatch(raw)) return "";

            char last = raw[raw.Length - 1];
            return char.IsDigit(last) || last == '.' ? raw : raw.Substring(0, raw.Length - 1);
        }

        // Splits the top-level arguments of a call whose '(' has already been consumed.
        // Returns null when the call never closes.
        static List<string> ReadArguments(string text, int start)
        {
            var args = new List<string>();
            int depth = 0;
            int argStart = start;

            for (int i = start; i < text.Length; i++)
            {
                char c = text[i];

                if (c == '"' || c == '\'')
                {
                    int close = SkipLiteral(text, i);
                    if (close < 0) return null;
                    i = close;
                    continue;
                }

                if (c == '(' || c == '[' || c == '{') { depth++; continue; }

                if (c == ')' && depth == 0)
                {
                    args.Add(text.Substring(argStart, i - argStart));
                    return args;
                }

                if (c == ')' || c == ']' || c == '}') { depth--; continue; }

                if (c == ',' && depth == 0)
                {
                    args.Add(text.Substring(argStart, i - argStart));
                    argStart = i + 1;
                }
            }

            return null;
        }

        // Index of the literal's closing quote, or -1 when unterminated.
        static int SkipLiteral(string text, int open)
        {
            char quote = text[open];
            bool verbatim = quote == '"' && open > 0 && text[open - 1] == '@';

            for (int i = open + 1; i < text.Length; i++)
            {
                char c = text[i];

                if (verbatim)
                {
                    if (c != quote) continue;
                    if (i + 1 < text.Length && text[i + 1] == quote) { i++; continue; }
                    return i;
                }

                if (c == '\\') { i++; continue; }
                if (c == quote) return i;
                if (c == '\n') return -1;
            }

            return -1;
        }

        // The argument text verbatim, or null when it isn't a single plain string literal
        // (an interpolated or concatenated key can't be resolved statically).
        static string Unquote(string arg)
        {
            if (arg == null) return null;
            arg = arg.Trim();

            int open = arg.StartsWith("@\"", StringComparison.Ordinal) ? 1 : 0;
            if (open >= arg.Length || arg[open] != '"') return null;

            int close = SkipLiteral(arg, open);
            if (close != arg.Length - 1) return null;

            string body = arg.Substring(open + 1, close - open - 1);
            return open == 1 ? body.Replace("\"\"", "\"") : Unescape(body);
        }

        static string Unescape(string body)
        {
            if (body.IndexOf('\\') < 0) return body;

            var sb = new StringBuilder(body.Length);
            for (int i = 0; i < body.Length; i++)
            {
                if (body[i] != '\\' || i + 1 >= body.Length) { sb.Append(body[i]); continue; }

                char next = body[++i];
                switch (next)
                {
                    case 'n': sb.Append('\n'); break;
                    case 't': sb.Append('\t'); break;
                    case 'r': sb.Append('\r'); break;
                    case '0': sb.Append('\0'); break;
                    default: sb.Append(next); break;      // \\ \" \' and anything exotic
                }
            }
            return sb.ToString();
        }

        static bool IsCommentedOut(string text, int index)
        {
            int lineStart = text.LastIndexOf('\n', Math.Max(0, index - 1)) + 1;
            if (lineStart >= index) return false;
            return text.IndexOf("//", lineStart, index - lineStart, StringComparison.Ordinal) >= 0;
        }

        static int LineOf(string text, int index)
        {
            int line = 1;
            for (int i = 0; i < index; i++)
                if (text[i] == '\n') line++;
            return line;
        }

        static string Quote(string s)
        {
            var sb = new StringBuilder("\"");
            foreach (char c in s ?? "")
            {
                switch (c)
                {
                    case '"': sb.Append("\\\""); break;
                    case '\\': sb.Append("\\\\"); break;
                    case '\n': sb.Append("\\n"); break;
                    case '\r': sb.Append("\\r"); break;
                    case '\t': sb.Append("\\t"); break;
                    default:
                        if (c < 0x20) sb.Append("\\u").Append(((int)c).ToString("x4"));
                        else sb.Append(c);
                        break;
                }
            }
            return sb.Append('"').ToString();
        }
    }
}
