using System.Collections.Concurrent;
using System.Text.RegularExpressions;
using Microsoft.Extensions.Logging;

namespace ZB.MOM.WW.ScadaBridge.AuditLog.Payload;

/// <summary>
/// Per-instance compiled-regex cache for audit body / SQL-parameter redactors
/// used by <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Redaction.ScadaBridgeAuditRedactor"/>.
/// Extracted in ScadaBridge audit re-architecture to
/// centralize compile rules (50 ms per-match timeout, 100 ms compile budget,
/// invalid-pattern sentinel).
/// </summary>
/// <remarks>
/// <para>
/// Lazy population keyed by pattern string: each pattern is compiled on first
/// use and cached forever. A failed compile (or a compile slower than 100 ms)
/// caches a sentinel so the failing compile is not retried on every event. The
/// failure is logged once on first encounter. <see cref="ConcurrentDictionary{TKey,TValue}"/>
/// is the right primitive because the owning redactor is a DI singleton on the
/// audit hot-path.
/// </para>
/// </remarks>
internal sealed class AuditRegexCache
{
    /// <summary>
    /// Per-match regex timeout. Catastrophic-backtracking patterns trip a
    /// <see cref="RegexMatchTimeoutException"/> when a single match takes longer
    /// than this; the caller then over-redacts the offending field. 50 ms is
    /// generous for normal patterns yet short enough that the audit hot-path is
    /// not held up by a misconfigured regex.
    /// </summary>
    private static readonly TimeSpan RegexMatchTimeout = TimeSpan.FromMilliseconds(50);

    private readonly ConcurrentDictionary<string, CompiledRegex> _cache = new();
    private readonly ILogger _logger;

    /// <summary>Initializes the cache with the logger used to report compile failures.</summary>
    /// <param name="logger">Logger for recording invalid or slow-compile pattern warnings.</param>
    public AuditRegexCache(ILogger logger) => _logger = logger;

    /// <summary>
    /// Resolve a compiled regex from the cache, compiling it on first use.
    /// Returns <c>false</c> for patterns that are invalid OR whose compile took
    /// longer than 100 ms (the spec calls catastrophic-backtracking guesses at
    /// compile time "invalid"); the failure is logged once and the sentinel
    /// cache entry prevents repeat compile attempts.
    /// </summary>
    /// <remarks>
    /// A <c>false</c> return for a pattern the operator actually configured means
    /// "this redactor is unavailable", NOT "there is nothing to redact". The
    /// caller MUST treat the two differently and fail closed — see
    /// <see cref="ZB.MOM.WW.ScadaBridge.AuditLog.Redaction.ScadaBridgeAuditRedactor"/>.
    /// </remarks>
    /// <param name="pattern">The regex pattern string to look up or compile.</param>
    /// <param name="regex">The compiled <see cref="Regex"/>, or <c>null</c> if the pattern is invalid.</param>
    /// <returns><c>true</c> if the pattern compiled successfully; <c>false</c> if it is invalid or too slow to compile.</returns>
    public bool TryGet(string pattern, out Regex? regex)
    {
        var entry = _cache.GetOrAdd(pattern, Compile);
        regex = entry.Regex;
        return entry.Regex != null;
    }

    /// <summary>
    /// Compile the supplied patterns up front so the 100 ms budget is measured
    /// once, off the audit hot path.
    /// </summary>
    /// <remarks>
    /// <para>
    /// The audit-log roadmap specifies these patterns are "precompiled at startup;
    /// rejected if compile takes >100ms" and that catastrophic-backtracking
    /// candidates are "rejected at startup". Compiling lazily on first event
    /// instead measured a wall-clock budget on a hot path under production load,
    /// where <see cref="RegexOptions.Compiled"/> IL emission can exceed 100 ms for
    /// a perfectly valid pattern — rejecting it, and (before the caller was made
    /// to fail closed) silently emitting the payload unredacted.
    /// </para>
    /// <para>
    /// Warming is idempotent and cheap to repeat: already-cached patterns short
    /// circuit in <see cref="ConcurrentDictionary{TKey,TValue}.GetOrAdd(TKey, Func{TKey, TValue})"/>.
    /// Call it once at construction and again whenever the options snapshot
    /// changes. Warming never throws — a pattern that fails here caches its
    /// sentinel exactly as it would have on the hot path.
    /// </para>
    /// </remarks>
    /// <param name="patterns">Patterns to precompile; <c>null</c> entries are skipped.</param>
    /// <returns>The number of supplied patterns that are unavailable (invalid or over budget).</returns>
    public int Warm(IEnumerable<string?>? patterns)
    {
        if (patterns == null)
        {
            return 0;
        }

        var unavailable = 0;
        foreach (var pattern in patterns)
        {
            if (string.IsNullOrEmpty(pattern))
            {
                continue;
            }
            if (!TryGet(pattern, out _))
            {
                unavailable++;
            }
        }
        return unavailable;
    }

    private CompiledRegex Compile(string pattern)
    {
        try
        {
            var swStart = System.Diagnostics.Stopwatch.GetTimestamp();
            var rx = new Regex(pattern, RegexOptions.Compiled, RegexMatchTimeout);
            var elapsedMs = (System.Diagnostics.Stopwatch.GetTimestamp() - swStart)
                * 1000d / System.Diagnostics.Stopwatch.Frequency;
            if (elapsedMs > 100)
            {
                _logger.LogWarning(
                    "Body redactor pattern compiled in {Elapsed}ms (> 100ms cap); rejecting '{Pattern}'",
                    elapsedMs, pattern);
                return CompiledRegex.Invalid;
            }
            return new CompiledRegex(rx);
        }
        catch (Exception ex)
        {
            _logger.LogWarning(
                ex,
                "Body redactor pattern '{Pattern}' failed to compile; skipping",
                pattern);
            return CompiledRegex.Invalid;
        }
    }

    /// <summary>
    /// Cache entry for a body-redactor pattern. Carries the working
    /// <see cref="Regex"/> on the success path, or the <see cref="Invalid"/>
    /// sentinel for patterns that failed to compile (or exceeded the 100 ms
    /// compile budget).
    /// </summary>
    private readonly struct CompiledRegex
    {
        public static readonly CompiledRegex Invalid = new(null);

        /// <summary>The compiled regex, or <c>null</c> when this entry represents an invalid pattern.</summary>
        public Regex? Regex { get; }

        /// <summary>Initializes the entry with the compiled regex (or <c>null</c> for the invalid sentinel).</summary>
        /// <param name="regex">The compiled <see cref="Regex"/>, or <c>null</c> for a failed compile.</param>
        public CompiledRegex(Regex? regex) => Regex = regex;
    }
}
