using System.Collections.Concurrent;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.OtOpcUa.Commons.Engines;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.ScriptedAlarms;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;
using SerilogLogger = Serilog.ILogger;
using SerilogLog = Serilog.Log;

namespace ZB.MOM.WW.OtOpcUa.Host.Engines;

/// <summary>
/// F9b — production <see cref="IScriptedAlarmEvaluator"/> binding. Compiles each unique
/// predicate once via <see cref="ScriptEvaluator{TContext, TResult}"/> against
/// <see cref="AlarmPredicateContext"/> and caches the resulting evaluator. Predicates are
/// pure functions returning <c>bool</c>: <see cref="AlarmPredicateContext.SetVirtualTag"/>
/// throws so a misbehaving script can't smuggle a side effect into alarm evaluation.
///
/// Failure modes (compile error, sandbox violation, runtime exception, timeout) all surface
/// as <see cref="ScriptedAlarmEvalResult.Failure"/>; <see cref="ScriptedAlarmActor"/>
/// preserves the prior state on failure (does not flip Active/Inactive).
/// </summary>
public sealed class RoslynScriptedAlarmEvaluator : IScriptedAlarmEvaluator, IDisposable
{
    private static readonly SerilogLogger ScriptLogger = SerilogLog.ForContext<RoslynScriptedAlarmEvaluator>();

    private readonly ConcurrentDictionary<string, ScriptEvaluator<AlarmPredicateContext, bool>> _cache
        = new(StringComparer.Ordinal);
    private readonly ILogger<RoslynScriptedAlarmEvaluator> _logger;
    private readonly TimeSpan _runTimeout;
    private bool _disposed;

    /// <summary>Initializes a new instance of the Roslyn scripted alarm evaluator.</summary>
    /// <param name="logger">Logger for diagnostic messages.</param>
    /// <param name="runTimeout">Optional timeout for script evaluation; defaults to 2 seconds.</param>
    public RoslynScriptedAlarmEvaluator(ILogger<RoslynScriptedAlarmEvaluator> logger, TimeSpan? runTimeout = null)
    {
        _logger = logger;
        _runTimeout = runTimeout ?? TimeSpan.FromSeconds(2);
    }

    /// <summary>Evaluates a scripted alarm predicate against provided dependencies.</summary>
    /// <param name="alarmId">The alarm identifier for logging purposes.</param>
    /// <param name="predicate">The predicate expression to evaluate.</param>
    /// <param name="dependencies">Variables available to the predicate expression.</param>
    /// <returns>Evaluation result with success flag and active state or failure reason.</returns>
    public ScriptedAlarmEvalResult Evaluate(string alarmId, string predicate, IReadOnlyDictionary<string, object?> dependencies)
    {
        if (_disposed) return ScriptedAlarmEvalResult.Failure("evaluator disposed");
        if (string.IsNullOrWhiteSpace(predicate)) return ScriptedAlarmEvalResult.Failure("empty predicate");

        ScriptEvaluator<AlarmPredicateContext, bool> evaluator;
        try
        {
            evaluator = _cache.GetOrAdd(predicate, ScriptEvaluator<AlarmPredicateContext, bool>.Compile);
        }
        catch (CompilationErrorException ex)
        {
            _logger.LogWarning(ex, "Alarm {Id}: predicate compile failed", alarmId);
            return ScriptedAlarmEvalResult.Failure($"compile error: {ex.Message}");
        }
        catch (ScriptSandboxViolationException ex)
        {
            _logger.LogWarning(ex, "Alarm {Id}: predicate sandbox violation", alarmId);
            return ScriptedAlarmEvalResult.Failure($"sandbox violation: {ex.Message}");
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Alarm {Id}: predicate compile threw", alarmId);
            return ScriptedAlarmEvalResult.Failure($"compile failure: {ex.Message}");
        }

        var readCache = BuildReadCache(dependencies);
        var context = new AlarmPredicateContext(readCache, ScriptLogger);

        try
        {
            using var cts = new CancellationTokenSource(_runTimeout);
            var active = evaluator.RunAsync(context, cts.Token).GetAwaiter().GetResult();
            return ScriptedAlarmEvalResult.Ok(active);
        }
        catch (OperationCanceledException)
        {
            return ScriptedAlarmEvalResult.Failure($"predicate timed out after {_runTimeout.TotalSeconds:F1}s");
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "Alarm {Id}: predicate execution threw", alarmId);
            return ScriptedAlarmEvalResult.Failure($"predicate threw: {ex.Message}");
        }
    }

    private static IReadOnlyDictionary<string, DataValueSnapshot> BuildReadCache(
        IReadOnlyDictionary<string, object?> deps)
    {
        var nowUtc = DateTime.UtcNow;
        var cache = new Dictionary<string, DataValueSnapshot>(StringComparer.Ordinal);
        foreach (var kv in deps)
        {
            cache[kv.Key] = new DataValueSnapshot(kv.Value, StatusCode: 0u, SourceTimestampUtc: nowUtc, ServerTimestampUtc: nowUtc);
        }
        return cache;
    }

    /// <summary>Disposes the evaluator and all cached script evaluators.</summary>
    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;
        foreach (var ev in _cache.Values)
        {
            try { ev.Dispose(); } catch { /* best-effort */ }
        }
        _cache.Clear();
    }
}
