using EonaCat.LogStack.Core;
using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;

namespace EonaCat.LogStack.Core;

// This file is part of the EonaCat project(s) which is released under the Apache License.
// See the LICENSE file or go to https://EonaCat.com/License for full license details.

/// <summary>
/// Hint for how a token's value should be captured.
/// </summary>
public enum TokenDestructureHint : byte
{
    /// <summary>ToString() / scalar value</summary>
    Default = 0,
    /// <summary>{@Property} - deep object destructuring (JSON-like)</summary>
    Destructure = 1,
    /// <summary>{$Property} - force ToString()</summary>
    Stringify = 2
}

/// <summary>
/// A single token parsed from a message template.
/// Supports advanced features:
/// - Nested property access: {Object.Property.SubProperty}
/// - Array indexing: {Array[0]}
/// - Alignment: {Name,10} (right), {Name,-10} (left)
/// - Filters: {Name|uppercase}, {Name|truncate:20}
/// - Conditionals: {?IsActive:Yes|No}
/// - Fallback: {Name??'default'}
/// </summary>
public readonly struct TemplateToken
{
    /// <summary>True = literal text; False = property hole.</summary>
    public readonly bool IsLiteral;
    /// <summary>Literal text segment, or null for property holes.</summary>
    public readonly string? Text;
    /// <summary>Property name for holes (may be a digit for positional placeholders, supports dot notation).</summary>
    public readonly string? Name;
    /// <summary>Zero-based positional index for positional placeholders; -1 for named.</summary>
    public readonly int Position;
    /// <summary>Destructure / stringify hint.</summary>
    public readonly TokenDestructureHint Hint;
    /// <summary>Optional format string (e.g. "D2").</summary>
    public readonly string? Format;
    /// <summary>Alignment width (positive=right, negative=left).</summary>
    public readonly int Alignment;
    /// <summary>Applied filters (e.g., "uppercase", "truncate:10").</summary>
    public readonly string[]? Filters;
    /// <summary>Fallback value if property is null/missing.</summary>
    public readonly string? Fallback;
    /// <summary>Optional conditional true/false values for {?PropertyName:True|False}.</summary>
    public readonly (string? TrueValue, string? FalseValue) ConditionalValues;

    private TemplateToken(string literal)
    {
        IsLiteral = true;
        Text = literal;
        Name = null;
        Position = -1;
        Hint = TokenDestructureHint.Default;
        Format = null;
        Alignment = 0;
        Filters = null;
        Fallback = null;
        ConditionalValues = (null, null);
    }

    private TemplateToken(string name, int position, TokenDestructureHint hint, string? format, int alignment = 0, string[]? filters = null, string? fallback = null, (string?, string?) conditionalValues = default)
    {
        IsLiteral = false;
        Text = null;
        Name = name;
        Position = position;
        Hint = hint;
        Format = format;
        Alignment = alignment;
        Filters = filters;
        Fallback = fallback;
        ConditionalValues = conditionalValues;
    }

    public static TemplateToken Literal(string text) => new(text);

    public static TemplateToken Property(string name, int position, TokenDestructureHint hint, string? format, int alignment = 0, string[]? filters = null, string? fallback = null, (string?, string?) conditionalValues = default)
        => new(name, position, hint, format, alignment, filters, fallback, conditionalValues);

    public override string ToString() =>
        IsLiteral ? $"Literal({Text})" : $"Hole({Hint}{Name}{(Format != null ? ":" + Format : "")})";
}

/// <summary>
/// Parses and renders Serilog-compatible message templates with advanced features.
///
/// Supported syntax:
///   {PropertyName}              - named property (default destructure)
///   {@PropertyName}             - named property, deep destructure
///   {$PropertyName}             - named property, force-stringify
///   {0}, {1}                    - positional
///   {PropertyName:format}       - with format specifier (e.g., "D2", "C")
///   {PropertyName,10}           - right-align with width 10
///   {PropertyName,-10}          - left-align with width 10
///   {Object.Property}           - nested property access (dot notation)
///   {Array[0]}                  - array/collection indexing
///   {PropertyName|uppercase}    - apply filter (uppercase, lowercase, truncate, etc.)
///   {PropertyName??'default'}   - fallback value if null/missing
///   {?IsActive:Yes|No}          - conditional rendering
///   {{ }}                       - escaped braces → literal { }
/// </summary>
public sealed class MessageTemplate
{
    private readonly string _raw;
    private readonly TemplateToken[] _tokens;

    public string Raw => _raw;
    public ReadOnlySpan<TemplateToken> Tokens => _tokens;

    private MessageTemplate(string raw, TemplateToken[] tokens)
    {
        _raw = raw;
        _tokens = tokens;
    }

    public static MessageTemplate Parse(string template)
    {
        if (template == null)
        {
            throw new ArgumentNullException(nameof(template));
        }

        var tokens = new List<TemplateToken>(8);
        var sb = new StringBuilder(template.Length);
        int i = 0;

        while (i < template.Length)
        {
            char c = template[i];

            if (c == '{')
            {
                // Escaped {{ → literal {
                if (i + 1 < template.Length && template[i + 1] == '{')
                {
                    sb.Append('{');
                    i += 2;
                    continue;
                }

                // Flush accumulated literal
                if (sb.Length > 0)
                {
                    tokens.Add(TemplateToken.Literal(sb.ToString()));
                    sb.Clear();
                }

                // Find matching }
                int end = FindClosingBrace(template, i + 1);
                if (end < 0)
                {
                    // Unclosed brace → treat rest as literal
                    sb.Append(template, i, template.Length - i);
                    i = template.Length;
                    continue;
                }

                string hole = template.Substring(i + 1, end - i - 1);
                tokens.Add(ParseHole(hole));
                i = end + 1;
            }
            else if (c == '}' && i + 1 < template.Length && template[i + 1] == '}')
            {
                // Escaped }}
                sb.Append('}');
                i += 2;
            }
            else
            {
                sb.Append(c);
                i++;
            }
        }

        if (sb.Length > 0)
        {
            tokens.Add(TemplateToken.Literal(sb.ToString()));
        }

        return new MessageTemplate(template, tokens.ToArray());
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static int FindClosingBrace(string template, int startPos)
    {
        for (int i = startPos; i < template.Length; i++)
        {
            if (template[i] == '}')
            {
                return i;
            }
        }
        return -1;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static TemplateToken ParseHole(string hole)
    {
        if (string.IsNullOrEmpty(hole))
        {
            return TemplateToken.Literal("{}");
        }

        var hint = TokenDestructureHint.Default;
        int nameStart = 0;
        bool isConditional = false;

        // Check for conditional {?Property:True|False}
        if (hole[0] == '?')
        {
            isConditional = true;
            nameStart = 1;
        }
        else if (hole[0] == '@') { hint = TokenDestructureHint.Destructure; nameStart = 1; }
        else if (hole[0] == '$') { hint = TokenDestructureHint.Stringify; nameStart = 1; }

        // Extract components: name, alignment, format, filters, fallback, conditional
        string name;
        string? format = null;
        int alignment = 0;
        string[]? filters = null;
        string? fallback = null;
        (string?, string?) conditionalValues = (null, null);

        // Parse: name[,alignment][|filters][??fallback][:format][:trueval|falseval]
        string remaining = hole.Substring(nameStart);

        // First, check for conditional values (only for conditional tokens)
        if (isConditional && remaining.Contains(":"))
        {
            int colonIdx = remaining.IndexOf(':');
            string beforeColon = remaining.Substring(0, colonIdx);
            string afterColon = remaining.Substring(colonIdx + 1);

            if (afterColon.Contains("|"))
            {
                int pipeIdx = afterColon.IndexOf('|');
                conditionalValues.Item1 = afterColon.Substring(0, pipeIdx).Trim();
                conditionalValues.Item2 = afterColon.Substring(pipeIdx + 1).Trim();
                remaining = beforeColon;
            }
        }

        // Parse filters (|uppercase, |truncate:10, etc.)
        if (remaining.Contains("|"))
        {
            int pipeIdx = remaining.IndexOf('|');
            string namePart = remaining.Substring(0, pipeIdx);
            string filterPart = remaining.Substring(pipeIdx + 1);
            remaining = namePart;
            filters = filterPart.Split('|');
        }

        // Parse fallback (??'default')
        if (remaining.Contains("??"))
        {
            int fallbackIdx = remaining.IndexOf("??");
            string namePart = remaining.Substring(0, fallbackIdx);
            fallback = remaining.Substring(fallbackIdx + 2).Trim();
            if (fallback.StartsWith("'") && fallback.EndsWith("'"))
            {
                fallback = fallback.Substring(1, fallback.Length - 2);
            }

            remaining = namePart;
        }

        // Parse alignment (,10 or ,-10)
        if (remaining.Contains(","))
        {
            int commaIdx = remaining.IndexOf(',');
            string namePart = remaining.Substring(0, commaIdx);
            string alignStr = remaining.Substring(commaIdx + 1).Trim();
            if (int.TryParse(alignStr, out int align))
            {
                alignment = align;
            }

            remaining = namePart;
        }

        // Parse format (:D2, :C, etc.)
        if (remaining.Contains(":") && !isConditional)
        {
            int colonIdx = remaining.IndexOf(':');
            string namePart = remaining.Substring(0, colonIdx);
            format = remaining.Substring(colonIdx + 1);
            remaining = namePart;
        }

        name = remaining.Trim();

        // Positional?
        int position = -1;
        if (name.Length > 0 && IsDigits(name))
        {
            int.TryParse(name, out position);
        }

        return TemplateToken.Property(name, position, hint, string.IsNullOrEmpty(format) ? null : format, alignment, filters, fallback, conditionalValues);
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool IsDigits(string s)
    {
        foreach (var c in s)
        {
            if (c < '0' || c > '9')
            {
                return false;
            }
        }

        return true;
    }

    /// <summary>
    /// Renders the template to a string, binding positional args and returning
    /// a dictionary of named properties.
    /// </summary>
    public string Render(object?[]? args, out Dictionary<string, object?> properties)
    {
        properties = new Dictionary<string, object?>(StringComparer.Ordinal);
        var sb = new StringBuilder(_raw.Length + 32);
        int positionalIndex = 0;

        foreach (ref readonly var token in _tokens.AsSpan())
        {
            if (token.IsLiteral)
            {
                sb.Append(token.Text);
                continue;
            }

            object? value;

            if (token.Position >= 0)
            {
                // Positional placeholder {0}, {1} …
                value = (args != null && token.Position < args.Length) ? args[token.Position] : null;
            }
            else if (args != null && positionalIndex < args.Length && IsArgumentDriven(args))
            {
                value = args[positionalIndex++];
                properties[token.Name!] = value;
            }
            else
            {
                value = ResolveNestedProperty(token.Name, properties);
            }

            if (token.Name != null && token.Position < 0)
            {
                properties[token.Name] = value;
            }

            AppendValue(sb, value, token.Hint, token.Format, token.Alignment, token.Filters, token.Fallback, token.ConditionalValues);
        }

        return sb.ToString();
    }

    /// <summary>
    /// Resolves nested property access (e.g., "Object.Property.SubProperty")
    /// </summary>
    private static object? ResolveNestedProperty(string? propertyPath, Dictionary<string, object?> properties)
    {
        if (propertyPath == null)
        {
            return null;
        }

        // Check for array indexing: Array[0]
        if (propertyPath.Contains("["))
        {
            int bracketIdx = propertyPath.IndexOf('[');
            string baseName = propertyPath.Substring(0, bracketIdx);
            string indexStr = propertyPath.Substring(bracketIdx + 1);
            if (indexStr.EndsWith("]"))
            {
                indexStr = indexStr.Substring(0, indexStr.Length - 1);
            }

            if (properties.TryGetValue(baseName, out var collection))
            {
                return GetCollectionItem(collection, indexStr);
            }
            return null;
        }

        // Handle nested property access with dots
        if (!propertyPath.Contains("."))
        {
            return properties.TryGetValue(propertyPath, out var val) ? val : null;
        }

        string[] parts = propertyPath.Split('.');
        object? current = null;

        if (!properties.TryGetValue(parts[0], out current))
        {
            return null;
        }

        for (int i = 1; i < parts.Length && current != null; i++)
        {
            current = GetPropertyValue(current, parts[i]);
        }

        return current;
    }

    private static object? GetPropertyValue(object? obj, string propertyName)
    {
        if (obj == null)
        {
            return null;
        }

        // Check for array indexing in nested path
        if (propertyName.Contains("["))
        {
            int bracketIdx = propertyName.IndexOf('[');
            string actualProp = propertyName.Substring(0, bracketIdx);
            string indexStr = propertyName.Substring(bracketIdx + 1);
            if (indexStr.EndsWith("]"))
            {
                indexStr = indexStr.Substring(0, indexStr.Length - 1);
            }

            var prop = obj.GetType().GetProperty(actualProp, System.Reflection.BindingFlags.IgnoreCase | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
            if (prop?.CanRead == true)
            {
                var collection = prop.GetValue(obj);
                return GetCollectionItem(collection, indexStr);
            }
            return null;
        }

        var propInfo = obj.GetType().GetProperty(propertyName, System.Reflection.BindingFlags.IgnoreCase | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance);
        return propInfo?.CanRead == true ? propInfo.GetValue(obj) : null;
    }

    private static object? GetCollectionItem(object? collection, string indexStr)
    {
        if (collection == null)
        {
            return null;
        }

        try
        {
            if (collection is System.Collections.IList list && int.TryParse(indexStr, out int index))
            {
                return index >= 0 && index < list.Count ? list[index] : null;
            }
        }
        catch { }

        return null;
    }

    /// <summary>
    /// Renders and populates the builder's properties from named holes.
    /// </summary>
    public string RenderInto(object?[]? args, LogEventBuilder builder)
    {
        var rendered = Render(args, out var props);
        foreach (var kv in props)
        {
            builder.WithProperty(kv.Key, kv.Value);
        }

        return rendered;
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static void AppendValue(StringBuilder sb, object? value, TokenDestructureHint hint, string? format, int alignment, string[]? filters, string? fallback, (string?, string?) conditionalValues)
    {
        // Handle conditional rendering
        if (conditionalValues.Item1 != null || conditionalValues.Item2 != null)
        {
            bool isTrue = IsTruthy(value);
            sb.Append(isTrue ? conditionalValues.Item1 : conditionalValues.Item2);
            return;
        }

        // Handle null/fallback
        if (value == null)
        {
            sb.Append(fallback ?? "null");
            return;
        }

        // Apply filters
        if (filters != null && filters.Length > 0)
        {
            value = ApplyFilters(value, filters);
        }

        string formatted = FormatValue(value, hint, format);

        // Apply alignment
        if (alignment != 0)
        {
            formatted = alignment > 0
                ? formatted.PadLeft(alignment)
                : formatted.PadRight(-alignment);
        }

        sb.Append(formatted);
    }

    private static bool IsTruthy(object? value)
    {
        if (value == null)
        {
            return false;
        }

        if (value is bool b)
        {
            return b;
        }

        if (value is int i)
        {
            return i != 0;
        }

        if (value is long l)
        {
            return l != 0;
        }

        if (value is string s)
        {
            return !string.IsNullOrEmpty(s);
        }

        return true;
    }

    private static object? ApplyFilters(object? value, string[] filters)
    {
        if (value == null)
        {
            return null;
        }

        foreach (var filter in filters)
        {
            string filterName = filter;
            string? filterParam = null;

            if (filter.Contains(":", StringComparison.Ordinal))
            {
                int colonIdx = filter.IndexOf(':');
                filterName = filter.Substring(0, colonIdx).Trim();
                filterParam = filter.Substring(colonIdx + 1).Trim();
            }

            value = ApplyFilter(value, filterName, filterParam);
        }

        return value;
    }

    private static object? ApplyFilter(object? value, string filterName, string? param)
    {
        if (value == null)
        {
            return null;
        }

        string str = value.ToString() ?? "";

        if (filterName.Equals("reverse", StringComparison.OrdinalIgnoreCase))
        {
            var chars = str.ToCharArray();
            System.Array.Reverse(chars);
            return new string(chars);
        }

        return filterName.ToLowerInvariant() switch
        {
            "uppercase" or "upper" => str.ToUpperInvariant(),
            "lowercase" or "lower" => str.ToLowerInvariant(),
            "trim" => str.Trim(),
            "truncate" => param != null && int.TryParse(param, out int len) ? (str.Length > len ? str.Substring(0, len) + "…" : str) : str,
            "substr" or "substring" => param != null && int.TryParse(param, out int pos) && pos < str.Length ? str.Substring(pos) : str,
            _ => str
        };
    }

    private static string FormatValue(object value, TokenDestructureHint hint, string? format)
    {
        if (hint == TokenDestructureHint.Destructure)
        {
            return Destructure(value);
        }

        if (value is IFormattable formattable && format != null)
        {
            try
            {
                return formattable.ToString(format, System.Globalization.CultureInfo.InvariantCulture);
            }
            catch
            {
                return value.ToString() ?? "";
            }
        }

        return value.ToString() ?? "";
    }

    private static string Destructure(object obj)
    {
        if (obj == null)
        {
            return "null";
        }

        var t = obj.GetType();
        if (t.IsPrimitive || obj is string || obj is decimal || obj is DateTime || obj is DateTimeOffset || obj is Guid)
        {
            return obj.ToString()!;
        }

        // Deep property bag with nesting
        var sb = new StringBuilder("{");
        bool first = true;
        foreach (var prop in t.GetProperties(System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.Instance))
        {
            try
            {
                if (!first)
                {
                    sb.Append(", ");
                }

                var val = prop.GetValue(obj);
                sb.Append(prop.Name).Append(": ");
                if (val == null)
                {
                    sb.Append("null");
                }
                else if (val.GetType().IsPrimitive || val is string || val is decimal || val is DateTime || val is DateTimeOffset)
                {
                    sb.Append(val);
                }
                else
                {
                    sb.Append(Destructure(val));
                }

                first = false;
            }
            catch { }
        }
        sb.Append('}');
        return sb.ToString();
    }

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    private static bool IsArgumentDriven(object?[] args) => args.Length > 0;

    // Template cache to avoid re-parsing the same strings
    private static readonly System.Collections.Concurrent.ConcurrentDictionary<string, MessageTemplate> _cache
        = new(StringComparer.Ordinal);

    /// <summary>Returns a cached parsed template (recommended for hot paths).</summary>
    public static MessageTemplate FromCache(string template) =>
        _cache.GetOrAdd(template, static t => Parse(t));

    /// <summary>Clears the template parse cache.</summary>
    public static void ClearCache() => _cache.Clear();
}
