// Port of Go conf/parse.go — recursive-descent parser for NATS config files.
// Reference: golang/nats-server/conf/parse.go

using System.Globalization;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;

namespace NATS.Server.Configuration;

/// <summary>
/// Parses NATS configuration data (tokenized by <see cref="NatsConfLexer"/>) into
/// a <c>Dictionary&lt;string, object?&gt;</c> tree. Supports nested maps, arrays,
/// variable references (block-scoped + environment), include directives, bcrypt
/// password literals, and integer suffix multipliers.
/// </summary>
public static class NatsConfParser
{
    private const string BcryptPrefix = "2a$";
    // Bcrypt hashes start with $2a$ or $2b$. The lexer consumes the leading '$'
    // and emits a Variable token whose value begins with "2a$" or "2b$".
    private const string BcryptPrefix2A = "2a$";
    private const string BcryptPrefix2B = "2b$";

    // Maximum nesting depth for include directives to prevent infinite recursion.
    private const int MaxIncludeDepth = 10;

    /// <summary>
    /// Parses a NATS configuration string into a dictionary.
    /// </summary>
    public static Dictionary<string, object?> Parse(string data)
    {
        var tokens = NatsConfLexer.Tokenize(data);
        var state = new ParserState(tokens, baseDir: string.Empty);
        state.Run();
        return state.Mapping;
    }

    /// <summary>
    /// Pedantic compatibility API (Go: ParseWithChecks).
    /// Uses the same parser behavior as <see cref="Parse(string)"/>.
    /// </summary>
    public static Dictionary<string, object?> ParseWithChecks(string data) => Parse(data);

    /// <summary>
    /// Parses a NATS configuration file into a dictionary.
    /// </summary>
    public static Dictionary<string, object?> ParseFile(string filePath) =>
        ParseFile(filePath, includeDepth: 0);

    /// <summary>
    /// Pedantic compatibility API (Go: ParseFileWithChecks).
    /// Uses the same parser behavior as <see cref="ParseFile(string)"/>.
    /// </summary>
    public static Dictionary<string, object?> ParseFileWithChecks(string filePath) => ParseFile(filePath);

    private static Dictionary<string, object?> ParseFile(string filePath, int includeDepth)
    {
        var data = File.ReadAllText(filePath);
        var tokens = NatsConfLexer.Tokenize(data);
        var baseDir = Path.GetDirectoryName(Path.GetFullPath(filePath)) ?? string.Empty;
        var state = new ParserState(tokens, baseDir, [], includeDepth);
        state.Run();
        return state.Mapping;
    }

    /// <summary>
    /// Parses a NATS configuration file and returns the parsed config plus a
    /// SHA-256 digest of the raw file content formatted as "sha256:&lt;hex&gt;".
    /// </summary>
    public static (Dictionary<string, object?> Config, string Digest) ParseFileWithDigest(string filePath)
    {
        var rawBytes = File.ReadAllBytes(filePath);
        var hashBytes = SHA256.HashData(rawBytes);
        var digest = "sha256:" + Convert.ToHexStringLower(hashBytes);

        var data = Encoding.UTF8.GetString(rawBytes);
        var tokens = NatsConfLexer.Tokenize(data);
        var baseDir = Path.GetDirectoryName(Path.GetFullPath(filePath)) ?? string.Empty;
        var state = new ParserState(tokens, baseDir, [], includeDepth: 0);
        state.Run();
        return (state.Mapping, digest);
    }

    /// <summary>
    /// Pedantic compatibility API (Go: ParseFileWithChecksDigest).
    /// </summary>
    public static (Dictionary<string, object?> Config, string Digest) ParseFileWithChecksDigest(string filePath)
    {
        var data = File.ReadAllText(filePath);
        var tokens = NatsConfLexer.Tokenize(data);
        var baseDir = Path.GetDirectoryName(Path.GetFullPath(filePath)) ?? string.Empty;
        var state = new ParserState(tokens, baseDir, [], includeDepth: 0);
        state.Run();
        CleanupUsedEnvVars(state.Mapping);

        var digest = ComputeConfigDigest(state.Mapping);
        return (state.Mapping, digest);
    }

    // Go pedantic mode removes env-var wrapper nodes from parsed maps before digesting.
    // The current parser does not persist env-wrapper nodes, so this remains a no-op hook.
    private static void CleanupUsedEnvVars(Dictionary<string, object?> _) { }

    private static string ComputeConfigDigest(Dictionary<string, object?> config)
    {
        using var ms = new MemoryStream();
        using (var writer = new Utf8JsonWriter(ms))
        {
            WriteCanonicalJsonValue(writer, config);
            writer.Flush();
        }

        var hashBytes = SHA256.HashData(ms.ToArray());
        return "sha256:" + Convert.ToHexStringLower(hashBytes);
    }

    private static void WriteCanonicalJsonValue(Utf8JsonWriter writer, object? value)
    {
        switch (value)
        {
            case null:
                writer.WriteNullValue();
                return;
            case Dictionary<string, object?> map:
                writer.WriteStartObject();
                foreach (var key in map.Keys.OrderBy(static k => k, StringComparer.Ordinal))
                {
                    writer.WritePropertyName(key);
                    WriteCanonicalJsonValue(writer, map[key]);
                }
                writer.WriteEndObject();
                return;
            case List<object?> list:
                writer.WriteStartArray();
                foreach (var item in list)
                    WriteCanonicalJsonValue(writer, item);
                writer.WriteEndArray();
                return;
            case IReadOnlyList<string> stringList:
                writer.WriteStartArray();
                foreach (var item in stringList)
                    writer.WriteStringValue(item);
                writer.WriteEndArray();
                return;
            case bool b:
                writer.WriteBooleanValue(b);
                return;
            case int i:
                writer.WriteNumberValue(i);
                return;
            case long l:
                writer.WriteNumberValue(l);
                return;
            case double d:
                writer.WriteNumberValue(d);
                return;
            case float f:
                writer.WriteNumberValue(f);
                return;
            case decimal dec:
                writer.WriteNumberValue(dec);
                return;
            case string s:
                writer.WriteStringValue(s);
                return;
            default:
                JsonSerializer.Serialize(writer, value, value.GetType());
                return;
        }
    }

    /// <summary>
    /// Internal: parse an environment variable value by wrapping it in a synthetic
    /// key-value assignment and parsing it. Shares the parent's env var cycle tracker.
    /// </summary>
    private static Dictionary<string, object?> ParseEnvValue(string value, HashSet<string> envVarReferences, int includeDepth)
    {
        var synthetic = $"pk={value}";
        var tokens = NatsConfLexer.Tokenize(synthetic);
        var state = new ParserState(tokens, baseDir: string.Empty, envVarReferences, includeDepth);
        state.Run();
        return state.Mapping;
    }

    /// <summary>
    /// Encapsulates the mutable parsing state: context stack, key stack, token cursor.
    /// Mirrors the Go <c>parser</c> struct from conf/parse.go.
    /// </summary>
    private sealed class ParserState
    {
        private readonly IReadOnlyList<Token> _tokens;
        private readonly string _baseDir;
        private readonly HashSet<string> _envVarReferences;
        private readonly int _includeDepth;
        private int _pos;

        // The context stack holds either Dictionary<string, object?> (map) or List<object?> (array).
        private readonly List<object> _ctxs = new(4);
        private object _ctx = null!;

        // Key stack for map assignments.
        private readonly List<string> _keys = new(4);
        // Pedantic-mode key token stack (Go parser field: ikeys).
        private readonly List<Token> _itemKeys = new(4);

        public Dictionary<string, object?> Mapping { get; } = new(StringComparer.OrdinalIgnoreCase);

        public ParserState(IReadOnlyList<Token> tokens, string baseDir)
            : this(tokens, baseDir, [], includeDepth: 0)
        {
        }

        public ParserState(IReadOnlyList<Token> tokens, string baseDir, HashSet<string> envVarReferences, int includeDepth)
        {
            _tokens = tokens;
            _baseDir = baseDir;
            _envVarReferences = envVarReferences;
            _includeDepth = includeDepth;
        }

        public void Run()
        {
            PushContext(Mapping);

            Token prevToken = default;
            while (true)
            {
                var token = Next();
                if (token.Type == TokenType.Eof)
                {
                    // Allow a trailing '}' (JSON-like configs) — mirror Go behavior.
                    if (prevToken.Type == TokenType.Key && prevToken.Value != "}")
                    {
                        throw new FormatException($"Config is invalid at line {token.Line}:{token.Position}");
                    }

                    break;
                }

                prevToken = token;
                ProcessItem(token);
            }
        }

        private Token Next()
        {
            if (_pos >= _tokens.Count)
            {
                return new Token(TokenType.Eof, string.Empty, 0, 0);
            }

            return _tokens[_pos++];
        }

        private void PushContext(object ctx)
        {
            _ctxs.Add(ctx);
            _ctx = ctx;
        }

        private object PopContext()
        {
            if (_ctxs.Count <= 1)
            {
                throw new InvalidOperationException("BUG in parser, context stack underflow");
            }

            var last = _ctxs[^1];
            _ctxs.RemoveAt(_ctxs.Count - 1);
            _ctx = _ctxs[^1];
            return last;
        }

        private void PushKey(string key) => _keys.Add(key);

        private string PopKey()
        {
            if (_keys.Count == 0)
            {
                throw new InvalidOperationException("BUG in parser, keys stack empty");
            }

            var last = _keys[^1];
            _keys.RemoveAt(_keys.Count - 1);
            return last;
        }

        private void PushItemKey(Token token) => _itemKeys.Add(token);

        private Token PopItemKey()
        {
            if (_itemKeys.Count == 0)
                return default;

            var last = _itemKeys[^1];
            _itemKeys.RemoveAt(_itemKeys.Count - 1);
            return last;
        }

        private void SetValue(object? val)
        {
            // Array context: append the value.
            if (_ctx is List<object?> array)
            {
                array.Add(val);
                return;
            }

            // Map context: pop the pending key and assign.
            if (_ctx is Dictionary<string, object?> map)
            {
                var key = PopKey();
                _ = PopItemKey();
                map[key] = val;
                return;
            }

            throw new InvalidOperationException($"BUG in parser, unexpected context type {_ctx?.GetType().Name ?? "null"}");
        }

        private void ProcessItem(Token token)
        {
            switch (token.Type)
            {
                case TokenType.Error:
                    throw new FormatException($"Parse error on line {token.Line}: '{token.Value}'");

                case TokenType.Key:
                    PushKey(token.Value);
                    PushItemKey(token);
                    break;

                case TokenType.String:
                    SetValue(token.Value);
                    break;

                case TokenType.Bool:
                    SetValue(ParseBool(token.Value));
                    break;

                case TokenType.Integer:
                    SetValue(ParseInteger(token.Value));
                    break;

                case TokenType.Float:
                    SetValue(ParseFloat(token.Value));
                    break;

                case TokenType.DateTime:
                    SetValue(DateTimeOffset.Parse(token.Value, CultureInfo.InvariantCulture));
                    break;

                case TokenType.ArrayStart:
                    PushContext(new List<object?>());
                    break;

                case TokenType.ArrayEnd:
                {
                    var array = _ctx;
                    PopContext();
                    SetValue(array);
                    break;
                }

                case TokenType.MapStart:
                    PushContext(new Dictionary<string, object?>(StringComparer.OrdinalIgnoreCase));
                    break;

                case TokenType.MapEnd:
                    SetValue(PopContext());
                    break;

                case TokenType.Variable:
                    ResolveVariable(token);
                    break;

                case TokenType.Include:
                    ProcessInclude(token.Value);
                    break;

                case TokenType.Comment:
                case TokenType.Text:
                    // Skip comments entirely.
                    break;

                case TokenType.Eof:
                    // Handled in the Run loop; should not reach here.
                    break;

                default:
                    throw new FormatException($"Unexpected token type {token.Type} on line {token.Line}");
            }
        }

        private static bool ParseBool(string value) =>
            value.ToLowerInvariant() switch
            {
                "true" or "yes" or "on" => true,
                "false" or "no" or "off" => false,
                _ => throw new FormatException($"Expected boolean value, but got '{value}'"),
            };

        /// <summary>
        /// Parses an integer token value, handling optional size suffixes
        /// (k, kb, m, mb, g, gb, t, tb, etc.) exactly as the Go reference does.
        /// </summary>
        private static long ParseInteger(string value)
        {
            // Find where digits end and potential suffix begins.
            var lastDigit = 0;
            foreach (var c in value)
            {
                if (!char.IsDigit(c) && c != '-')
                {
                    break;
                }

                lastDigit++;
            }

            var numStr = value[..lastDigit];
            if (!long.TryParse(numStr, NumberStyles.Integer, CultureInfo.InvariantCulture, out var num))
            {
                throw new FormatException($"Expected integer, but got '{value}'");
            }

            var suffix = value[lastDigit..].Trim().ToLowerInvariant();
            return suffix switch
            {
                "" => num,
                "k" => num * 1000,
                "kb" or "ki" or "kib" => num * 1024,
                "m" => num * 1_000_000,
                "mb" or "mi" or "mib" => num * 1024 * 1024,
                "g" => num * 1_000_000_000,
                "gb" or "gi" or "gib" => num * 1024 * 1024 * 1024,
                "t" => num * 1_000_000_000_000,
                "tb" or "ti" or "tib" => num * 1024L * 1024 * 1024 * 1024,
                "p" => num * 1_000_000_000_000_000,
                "pb" or "pi" or "pib" => num * 1024L * 1024 * 1024 * 1024 * 1024,
                "e" => num * 1_000_000_000_000_000_000,
                "eb" or "ei" or "eib" => num * 1024L * 1024 * 1024 * 1024 * 1024 * 1024,
                _ => throw new FormatException($"Unknown integer suffix '{suffix}' in '{value}'"),
            };
        }

        private static double ParseFloat(string value)
        {
            if (!double.TryParse(value, NumberStyles.Float | NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var result))
            {
                throw new FormatException($"Expected float, but got '{value}'");
            }

            return result;
        }

        /// <summary>
        /// Resolves a variable reference using block scoping: walks the context stack
        /// top-down looking in map contexts, then falls back to environment variables.
        /// Detects bcrypt password literals and reference cycles.
        /// </summary>
        private void ResolveVariable(Token token)
        {
            var varName = token.Value;

            // Special case: raw bcrypt strings ($2a$... or $2b$...).
            // The lexer consumed the leading '$', so the variable value starts with "2a$" or "2b$".
            if (varName.StartsWith(BcryptPrefix, StringComparison.Ordinal) ||
                varName.StartsWith(BcryptPrefix2A, StringComparison.Ordinal) ||
                varName.StartsWith(BcryptPrefix2B, StringComparison.Ordinal))
            {
                SetValue("$" + varName);
                return;
            }

            // Walk context stack from top (innermost scope) to bottom (outermost).
            for (var i = _ctxs.Count - 1; i >= 0; i--)
            {
                if (_ctxs[i] is Dictionary<string, object?> map && map.TryGetValue(varName, out var found))
                {
                    SetValue(found);
                    return;
                }
            }

            // Not found in any context map. Check environment variables.
            // First, detect cycles.
            if (!_envVarReferences.Add(varName))
            {
                throw new FormatException($"Variable reference cycle for '{varName}'");
            }

            try
            {
                var envValue = Environment.GetEnvironmentVariable(varName);
                if (envValue is not null)
                {
                    // Parse the env value through the full parser to get correct typing
                    // (e.g., "42" becomes long 42, "true" becomes bool, etc.).
                    var subResult = ParseEnvValue(envValue, _envVarReferences, _includeDepth);
                    if (subResult.TryGetValue("pk", out var parsedValue))
                    {
                        SetValue(parsedValue);
                        return;
                    }
                }
            }
            finally
            {
                _envVarReferences.Remove(varName);
            }

            // Not found anywhere.
            throw new FormatException(
                $"Variable reference for '{varName}' on line {token.Line} can not be found");
        }

        /// <summary>
        /// Processes an include directive by parsing the referenced file and merging
        /// all its top-level keys into the current context.
        /// </summary>
        private void ProcessInclude(string includePath)
        {
            if (_includeDepth >= MaxIncludeDepth)
            {
                throw new FormatException(
                    $"Include depth limit of {MaxIncludeDepth} exceeded while processing '{includePath}'");
            }

            var fullPath = Path.Combine(_baseDir, includePath);
            var includeResult = ParseFile(fullPath, _includeDepth + 1);

            foreach (var (key, value) in includeResult)
            {
                PushKey(key);
                SetValue(value);
            }
        }
    }
}
