using System.Text.RegularExpressions;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;

/// <summary>
///     Alarm messages are static-with-substitution templates. The engine resolves
///     <c>{TagPath}</c> tokens at event emission time
///     against current tag values; unresolvable tokens become <c>{?}</c> so the event
///     still fires but the operator sees where the reference broke.
/// </summary>
/// <remarks>
///     <para>
///         Token syntax: <c>{path/with/slashes}</c>. Brace-stripped the contents must
///         match a path the caller's resolver function can look up. No escaping
///         currently — if you need literal braces in the message, reach for a feature
///         request.
///     </para>
///     <para>
///         Pure function. Same inputs always produce the same string. Tests verify the
///         edge cases (no tokens / one token / many / nested / unresolvable / bad
///         quality / null value).
///     </para>
/// </remarks>
public static class MessageTemplate
{
    private static readonly Regex TokenRegex = new(@"\{([^{}]+)\}",
        RegexOptions.Compiled | RegexOptions.CultureInvariant);

    /// <summary>
    ///     Resolve every <c>{path}</c> token in <paramref name="template"/> using
    ///     <paramref name="resolveTag"/>. Tokens whose returned <see cref="DataValueSnapshot"/>
    ///     has a non-Good <see cref="DataValueSnapshot.StatusCode"/> or a null
    ///     <see cref="DataValueSnapshot.Value"/> resolve to <c>{?}</c>.
    /// </summary>
    /// <remarks>
    ///     Quality bar is intentionally <em>stricter</em> than predicate evaluation:
    ///     only Good (StatusCode == 0) is substituted; Uncertain renders as
    ///     <c>{?}</c>. The predicate gate (<c>ScriptedAlarmEngine.AreInputsReady</c>)
    ///     accepts Uncertain because it still carries a value the predicate can
    ///     inspect, but the operator-facing message must make doubt explicit rather
    ///     than substituting a value an operator might act on. See the
    ///     "Input-quality policy" section in <c>docs/ScriptedAlarms.md</c>.
    /// </remarks>
    /// <param name="template">The template string with {path} tokens.</param>
    /// <param name="resolveTag">A function to resolve tag values by path.</param>
    /// <returns>The resolved template string with tokens replaced or marked as unresolvable.</returns>
    public static string Resolve(string template, Func<string, DataValueSnapshot?> resolveTag)
    {
        if (string.IsNullOrEmpty(template)) return template ?? string.Empty;
        if (resolveTag is null) throw new ArgumentNullException(nameof(resolveTag));

        return TokenRegex.Replace(template, match =>
        {
            var path = match.Groups[1].Value.Trim();
            if (path.Length == 0) return "{?}";
            var snap = resolveTag(path);
            if (snap is null) return "{?}";
            if (snap.StatusCode != 0u) return "{?}";
            return snap.Value?.ToString() ?? "{?}";
        });
    }

    /// <summary>Enumerate the token paths the template references. Used at publish time to validate references exist.</summary>
    /// <param name="template">The template string to extract tokens from.</param>
    /// <returns>A list of all token paths found in the template.</returns>
    public static IReadOnlyList<string> ExtractTokenPaths(string? template)
    {
        if (string.IsNullOrEmpty(template)) return Array.Empty<string>();
        var tokens = new List<string>();
        foreach (Match m in TokenRegex.Matches(template))
        {
            var path = m.Groups[1].Value.Trim();
            if (path.Length > 0) tokens.Add(path);
        }
        return tokens;
    }
}
