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.
///
/// <para>
///     02/S12 — the apply-boundary <see cref="ClearCompiledScripts"/> (driven by the host actor per
///     deploy generation) disposes cached evaluators while unchanged children may still be evaluating.
///     A clear landing between this adapter's <c>GetOrCompile</c> and the run's disposed guard would
///     otherwise surface as a spurious <c>Failure</c>. The compiled path is therefore retry-safe: an
///     <see cref="ObjectDisposedException"/> from an in-flight evaluation re-fetches (recompiling the
///     same source into the current cache generation) and retries once. A second disposal mid-retry
///     (two applies inside one evaluation — not a realistic cadence) falls through to the failure path.
///     Residual (accepted, bounded to one generation): a stopping child can recompile its stale source
///     into the fresh cache for one generation; P7's expression-set gate means no-op redeploys no
///     longer open this window at all.
/// </para>
/// </summary>
public sealed class RoslynVirtualTagEvaluator : IVirtualTagEvaluator, IScriptCacheOwner, IDisposable
{
    // Purpose-built source-hash-keyed compile cache: Lazy single-compile (no double-compile race) and
    // Clear()/Dispose() that dispose each evaluator's collectible AssemblyLoadContext. The apply-boundary
    // Clear() (driven by VirtualTagHostActor via IScriptCacheOwner) is what stops ALCs accreting across
    // script edits — the raw ConcurrentDictionary this replaced never released them.
    private readonly CompiledScriptCache<VirtualTagContext, object?> _cache = new();
    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);
    }

    /// <inheritdoc />
    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);
        }

        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);

        // 02/S12: fetch + run inside a bounded loop so an ObjectDisposedException caused by a concurrent
        // apply-boundary ClearCompiledScripts (which disposed the evaluator between GetOrCompile and the
        // run's disposed guard) re-fetches and retries exactly once. The loop runs at most twice — the
        // retry catch only matches attempt == 0.
        for (var attempt = 0; ; attempt++)
        {
            ScriptEvaluator<VirtualTagContext, object?> evaluator;
            try
            {
                evaluator = _cache.GetOrCompile(expression);
            }
            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 (ObjectDisposedException)
            {
                // The cache itself is being torn down (this evaluator is disposing) — a fetch-time
                // disposal is a shutdown signal, not the apply-boundary race, so never retry.
                return VirtualTagEvalResult.Failure("evaluator disposed");
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "VirtualTag {Id}: compile threw", virtualTagId);
                return VirtualTagEvalResult.Failure($"compile failure: {ex.Message}");
            }

            try
            {
                // Route through TimedScriptEvaluator (Task.Run + WaitAsync): a raw CancellationToken can't
                // interrupt a CPU-bound/infinite-loop script (Roslyn scripts don't poll it), so the previous
                // CTS-only path let one runaway script hang this actor forever. WaitAsync returns control when
                // the wall-clock budget fires regardless of whether the inner task completes (the orphaned
                // thread is the documented, accepted trade-off — TimedScriptEvaluator remarks).
                var timed = new TimedScriptEvaluator<VirtualTagContext, object?>(evaluator, _runTimeout);
                var raw = timed.RunAsync(context).GetAwaiter().GetResult();
                return VirtualTagEvalResult.Ok(raw);
            }
            catch (ScriptTimeoutException)
            {
                return VirtualTagEvalResult.Failure($"script timed out after {_runTimeout.TotalSeconds:F1}s");
            }
            catch (ObjectDisposedException) when (!_disposed && attempt == 0)
            {
                // S12: the apply-boundary ClearCompiledScripts disposed the evaluator between our
                // GetOrCompile and the run's disposed-guard. Re-fetch — GetOrCompile recompiles the same
                // source into the CURRENT cache generation — and retry once. A second disposal mid-retry
                // (two applies inside one evaluation) falls through to the failure path below.
                continue;
            }
            catch (ObjectDisposedException)
            {
                return VirtualTagEvalResult.Failure("evaluator disposed during evaluation");
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "VirtualTag {Id}: script execution threw", virtualTagId);
                return VirtualTagEvalResult.Failure($"script threw: {ex.Message}");
            }
        }
    }

    /// <inheritdoc />
    public void ClearCompiledScripts() => _cache.Clear();

    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;
        // CompiledScriptCache.Dispose drains + disposes every materialised evaluator's ALC.
        _cache.Dispose();
    }
}
