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.Scripting;
using ZB.MOM.WW.OtOpcUa.Core.VirtualTags;
using SerilogLogger = Serilog.ILogger;

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

/// <summary>
/// F8b — production <see cref="IVirtualTagEvaluator"/> binding. Compiles each unique
/// expression once via <see cref="ScriptEvaluator{TContext, TResult}"/> (Roslyn-backed
/// sandbox) and caches the resulting evaluator keyed by source. Subsequent evaluations are
/// in-process method invocations on the dependency dictionary — fast enough to run inline
/// inside the actor's message handler.
///
/// Single-tag mode: cross-tag <c>ctx.SetVirtualTag</c> writes are dropped (logged) because
/// fan-out between actors is owned by <c>DependencyMuxActor</c>, not by the eval engine.
/// Cycle detection + cascade ordering live in <see cref="VirtualTagEngine"/>; this adapter
/// stays single-tag scoped to keep <see cref="VirtualTagActor"/>'s message loop simple.
/// </summary>
public sealed class RoslynVirtualTagEvaluator : IVirtualTagEvaluator, IDisposable
{
    private readonly ConcurrentDictionary<string, ScriptEvaluator<VirtualTagContext, object?>> _cache
        = new(StringComparer.Ordinal);
    private readonly ILogger<RoslynVirtualTagEvaluator> _logger;
    private readonly SerilogLogger _scriptRoot;
    private readonly TimeSpan _runTimeout;
    private bool _disposed;

    /// <summary>Initializes a new RoslynVirtualTagEvaluator with the given loggers and optional timeout.</summary>
    /// <param name="logger">Logger for recording compilation and execution errors (host diagnostics).</param>
    /// <param name="scriptRoot">Root script logger; user <c>ctx.Logger.*</c> output flows through this to the Script-log page.</param>
    /// <param name="runTimeout">Maximum execution time for each script; defaults to 2 seconds if not specified.</param>
    public RoslynVirtualTagEvaluator(
        ILogger<RoslynVirtualTagEvaluator> logger,
        ScriptRootLogger scriptRoot,
        TimeSpan? runTimeout = null)
    {
        _logger = logger;
        _scriptRoot = (scriptRoot ?? throw new ArgumentNullException(nameof(scriptRoot))).Logger;
        _runTimeout = runTimeout ?? TimeSpan.FromSeconds(2);
    }

    /// <summary>Evaluates a virtual tag expression against a set of dependencies.</summary>
    /// <param name="virtualTagId">The virtual tag identifier, used for logging.</param>
    /// <param name="expression">The C# expression to evaluate.</param>
    /// <param name="dependencies">Dictionary of tag names to values available in the expression context.</param>
    /// <returns>The evaluation result, either successful with a value or failed with an error message.</returns>
    public VirtualTagEvalResult Evaluate(string virtualTagId, string expression, IReadOnlyDictionary<string, object?> dependencies)
    {
        if (_disposed) return VirtualTagEvalResult.Failure("evaluator disposed");
        if (string.IsNullOrWhiteSpace(expression)) return VirtualTagEvalResult.Failure("empty expression");

        // A — passthrough fast-path: the mirror shape `return ctx.GetTag("X").Value;` needs no
        // Roslyn. Narrow exact pattern; near-misses fall through to the compiled path below.
        // Semantics are byte-identical to the compiled mirror: a present dependency returns its
        // value (Ok), and an absent one makes GetTag yield a Bad snapshot whose .Value is null,
        // so the script returns null — Ok(null) here too.
        if (PassthroughScript.TryMatch(expression, out var passthroughRef))
        {
            return dependencies.TryGetValue(passthroughRef, out var ptValue)
                ? VirtualTagEvalResult.Ok(ptValue)
                : VirtualTagEvalResult.Ok(null);
        }

        ScriptEvaluator<VirtualTagContext, object?> evaluator;
        try
        {
            evaluator = _cache.GetOrAdd(expression, ScriptEvaluator<VirtualTagContext, object?>.Compile);
        }
        catch (CompilationErrorException ex)
        {
            _logger.LogWarning(ex, "VirtualTag {Id}: Roslyn compile failed", virtualTagId);
            return VirtualTagEvalResult.Failure($"compile error: {ex.Message}");
        }
        catch (ScriptSandboxViolationException ex)
        {
            _logger.LogWarning(ex, "VirtualTag {Id}: sandbox violation", virtualTagId);
            return VirtualTagEvalResult.Failure($"sandbox violation: {ex.Message}");
        }
        catch (Exception ex)
        {
            _logger.LogWarning(ex, "VirtualTag {Id}: compile threw", virtualTagId);
            return VirtualTagEvalResult.Failure($"compile failure: {ex.Message}");
        }

        var readCache = BuildReadCache(dependencies);
        // Per-evaluation script logger: bind both ScriptId and VirtualTagId from the virtual-tag id
        // (in the live path the script id equals the virtual-tag id) so the Script-log page can
        // attribute each line. EquipmentId stays unbound for now.
        var scriptLog = _scriptRoot
            .ForContext(ScriptLoggerFactory.ScriptIdProperty, virtualTagId)
            .ForContext(ScriptLoggerFactory.VirtualTagIdProperty, virtualTagId);
        var context = new VirtualTagContext(
            readCache,
            setVirtualTag: (path, _) =>
                _logger.LogDebug("VirtualTag {Id}: cross-tag write to {Path} dropped (single-tag adapter)",
                    virtualTagId, path),
            logger: scriptLog);

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

    private static IReadOnlyDictionary<string, DataValueSnapshot> BuildReadCache(
        IReadOnlyDictionary<string, object?> deps)
    {
        // VirtualTagContext.GetTag returns a DataValueSnapshot — we wrap each raw dep value
        // as Good-quality so the script's `(int)ctx.GetTag("a").Value` pattern works. Null
        // values stay null; the script can null-check via GetTag(path).Value.
        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 cleans up 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();
    }
}
