using System.Collections.Concurrent;
using Serilog;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Core.Scripting;

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

/// <summary>
///     The Phase 7 virtual-tag evaluation engine. Ingests a set of
///     <see cref="VirtualTagDefinition"/>s at load time, compiles each script against
///     <see cref="ScriptSandbox"/>, builds the dependency graph, subscribes to every
///     referenced upstream tag, and schedules re-evaluations on change + on timer.
/// </summary>
/// <remarks>
///     <para>
///         Evaluation order is topological per ADR-001 / Phase 7 plan decision #19 —
///         serial for the v1 rollout, parallel promoted to a follow-up. When upstream
///         tag X changes, the engine computes the transitive dependent closure of X in
///         topological rank and evaluates each in turn, so a cascade through multiple
///         levels of virtual tags settles within one change-trigger pass.
///     </para>
///     <para>
///         Per-tag error isolation per Phase 7 plan decision #11 — a script exception
///         (or timeout) fails that tag's latest value with <c>BadInternalError</c> or
///         <c>BadTypeMismatch</c> quality and logs a structured error; every other tag
///         keeps evaluating. The engine itself never faults from a user script.
///     </para>
/// </remarks>
public sealed class VirtualTagEngine : IDisposable
{
    private readonly ITagUpstreamSource _upstream;
    private readonly IHistoryWriter _history;
    private readonly ScriptLoggerFactory _loggerFactory;
    private readonly ILogger _engineLogger;
    private readonly Func<DateTime> _clock;
    private readonly TimeSpan _scriptTimeout;

    private readonly DependencyGraph _graph = new();
    private readonly Dictionary<string, VirtualTagState> _tags = new(StringComparer.Ordinal);
    private readonly ConcurrentDictionary<string, DataValueSnapshot> _valueCache = new(StringComparer.Ordinal);
    private readonly ConcurrentDictionary<string, List<Action<string, DataValueSnapshot>>> _observers
        = new(StringComparer.Ordinal);
    private readonly List<IDisposable> _upstreamSubscriptions = [];
    private readonly SemaphoreSlim _evalGate = new(1, 1);
    private bool _loaded;
    private bool _disposed;

    public VirtualTagEngine(
        ITagUpstreamSource upstream,
        ScriptLoggerFactory loggerFactory,
        ILogger engineLogger,
        IHistoryWriter? historyWriter = null,
        Func<DateTime>? clock = null,
        TimeSpan? scriptTimeout = null)
    {
        _upstream = upstream ?? throw new ArgumentNullException(nameof(upstream));
        _loggerFactory = loggerFactory ?? throw new ArgumentNullException(nameof(loggerFactory));
        _engineLogger = engineLogger ?? throw new ArgumentNullException(nameof(engineLogger));
        _history = historyWriter ?? NullHistoryWriter.Instance;
        _clock = clock ?? (() => DateTime.UtcNow);
        _scriptTimeout = scriptTimeout ?? TimedScriptEvaluator<VirtualTagContext, object?>.DefaultTimeout;
    }

    /// <summary>Registered tag paths, in topological order. Empty before <see cref="Load"/>.</summary>
    public IReadOnlyCollection<string> LoadedTagPaths => _tags.Keys;

    /// <summary>Compile + register every tag in <paramref name="definitions"/>. Throws on cycle or any compile failure.</summary>
    public void Load(IReadOnlyList<VirtualTagDefinition> definitions)
    {
        if (_disposed) throw new ObjectDisposedException(nameof(VirtualTagEngine));
        if (definitions is null) throw new ArgumentNullException(nameof(definitions));

        // Start from a clean slate — supports config-publish reloads.
        UnsubscribeFromUpstream();
        _tags.Clear();
        _graph.Clear();

        var compileFailures = new List<string>();
        foreach (var def in definitions)
        {
            try
            {
                var extraction = DependencyExtractor.Extract(def.ScriptSource);
                if (!extraction.IsValid)
                {
                    var msgs = string.Join("; ", extraction.Rejections.Select(r => r.Message));
                    compileFailures.Add($"{def.Path}: dependency extraction rejected — {msgs}");
                    continue;
                }

                var evaluator = ScriptEvaluator<VirtualTagContext, object?>.Compile(def.ScriptSource);
                var timed = new TimedScriptEvaluator<VirtualTagContext, object?>(evaluator, _scriptTimeout);
                var scriptLogger = _loggerFactory.Create(def.Path);

                _tags[def.Path] = new VirtualTagState(def, timed, extraction.Reads, extraction.Writes, scriptLogger);
                _graph.Add(def.Path, extraction.Reads);
            }
            catch (Exception ex)
            {
                compileFailures.Add($"{def.Path}: {ex.Message}");
            }
        }

        if (compileFailures.Count > 0)
        {
            var joined = string.Join("\n  ", compileFailures);
            throw new InvalidOperationException(
                $"Virtual-tag engine load failed. {compileFailures.Count} script(s) did not compile:\n  {joined}");
        }

        // Cycle check — throws DependencyCycleException on offense.
        _ = _graph.TopologicalSort();

        // Subscribe to every referenced upstream path (driver tags only — virtual tags
        // cascade internally). Seed the cache with current upstream values so first
        // evaluations see something real.
        var upstreamPaths = definitions
            .SelectMany(d => _tags[d.Path].Reads)
            .Where(p => !_tags.ContainsKey(p))
            .Distinct(StringComparer.Ordinal);
        foreach (var path in upstreamPaths)
        {
            _valueCache[path] = _upstream.ReadTag(path);
            _upstreamSubscriptions.Add(_upstream.SubscribeTag(path, OnUpstreamChange));
        }

        _loaded = true;
        _engineLogger.Information(
            "VirtualTagEngine loaded {TagCount} tag(s), {UpstreamCount} upstream subscription(s)",
            _tags.Count, _upstreamSubscriptions.Count);
    }

    /// <summary>
    ///     Evaluate every registered tag once in topological order — used at startup so
    ///     virtual tags have a defined initial value rather than inheriting the cache
    ///     default. Also called after a config reload.
    /// </summary>
    public async Task EvaluateAllAsync(CancellationToken ct = default)
    {
        EnsureLoaded();
        var order = _graph.TopologicalSort();
        foreach (var path in order)
        {
            if (_tags.ContainsKey(path))
                await EvaluateOneAsync(path, ct).ConfigureAwait(false);
        }
    }

    /// <summary>Evaluate a single tag — used by the timer trigger + test hooks.</summary>
    public Task EvaluateOneAsync(string path, CancellationToken ct = default)
    {
        EnsureLoaded();
        if (!_tags.ContainsKey(path))
            throw new ArgumentException($"Not a registered virtual tag: {path}", nameof(path));
        return EvaluateInternalAsync(path, ct);
    }

    /// <summary>
    ///     Read the most recently evaluated value for <paramref name="path"/>. Driver
    ///     tags return the last-known upstream value; virtual tags return their last
    ///     evaluation result.
    /// </summary>
    public DataValueSnapshot Read(string path)
    {
        if (string.IsNullOrWhiteSpace(path))
            return new DataValueSnapshot(null, 0x80340000u, null, _clock());
        return _valueCache.TryGetValue(path, out var v)
            ? v
            : new DataValueSnapshot(null, 0x80340000u /* BadNodeIdUnknown */, null, _clock());
    }

    /// <summary>
    ///     Register an observer that fires on every evaluation of the given tag.
    ///     Returns an <see cref="IDisposable"/> to unsubscribe. Does NOT fire a seed
    ///     value — subscribers call <see cref="Read"/> for the current value if needed.
    /// </summary>
    public IDisposable Subscribe(string path, Action<string, DataValueSnapshot> observer)
    {
        var list = _observers.GetOrAdd(path, _ => []);
        lock (list) { list.Add(observer); }
        return new Unsub(this, path, observer);
    }

    /// <summary>
    ///     Change-trigger entry point — called by the upstream subscription callback.
    ///     Updates the cache, fans out to observers (so OPC UA clients see the upstream
    ///     change too if they subscribed via the engine), and schedules every
    ///     change-triggered dependent for re-evaluation in topological order.
    /// </summary>
    internal void OnUpstreamChange(string path, DataValueSnapshot value)
    {
        _valueCache[path] = value;
        NotifyObservers(path, value);

        // Fire-and-forget — the upstream subscription callback must not block the
        // driver's dispatcher. Exceptions during cascade are handled per-tag inside
        // EvaluateInternalAsync.
        _ = CascadeAsync(path, CancellationToken.None);
    }

    private async Task CascadeAsync(string upstreamPath, CancellationToken ct)
    {
        try
        {
            var dependents = _graph.TransitiveDependentsInOrder(upstreamPath);
            foreach (var dep in dependents)
            {
                if (_tags.TryGetValue(dep, out var state) && state.Definition.ChangeTriggered)
                    await EvaluateInternalAsync(dep, ct).ConfigureAwait(false);
            }
        }
        catch (Exception ex)
        {
            _engineLogger.Error(ex, "VirtualTagEngine cascade failed for upstream {Path}", upstreamPath);
        }
    }

    private async Task EvaluateInternalAsync(string path, CancellationToken ct)
    {
        if (!_tags.TryGetValue(path, out var state)) return;

        // Serial evaluation across all tags. Phase 7 plan decision #19 — parallel is a
        // follow-up. The semaphore bounds the evaluation graph so two cascades don't
        // interleave, which would break the "earlier nodes computed first" invariant.
        // SemaphoreSlim.WaitAsync is async-safe where Monitor.Enter is not (Monitor
        // ownership is thread-local and lost across await).
        await _evalGate.WaitAsync(ct).ConfigureAwait(false);
        try
        {
            var ctxCache = BuildReadCache(state.Reads);
            var context = new VirtualTagContext(
                ctxCache,
                (p, v) => OnScriptSetVirtualTag(p, v),
                state.Logger,
                _clock);

            DataValueSnapshot result;
            try
            {
                var raw = await state.Evaluator.RunAsync(context, ct).ConfigureAwait(false);
                var coerced = CoerceResult(raw, state.Definition.DataType);
                result = new DataValueSnapshot(coerced, 0u, _clock(), _clock());
            }
            catch (ScriptTimeoutException tex)
            {
                state.Logger.Warning("Script timed out after {Timeout}", tex.Timeout);
                result = new DataValueSnapshot(null, 0x80020000u /* BadInternalError */, null, _clock());
            }
            catch (OperationCanceledException)
            {
                throw; // shutdown path — don't misclassify
            }
            catch (Exception ex)
            {
                state.Logger.Error(ex, "Virtual-tag script threw");
                result = new DataValueSnapshot(null, 0x80020000u /* BadInternalError */, null, _clock());
            }

            _valueCache[path] = result;
            NotifyObservers(path, result);
            if (state.Definition.Historize) _history.Record(path, result);
        }
        finally
        {
            _evalGate.Release();
        }
    }

    private IReadOnlyDictionary<string, DataValueSnapshot> BuildReadCache(IReadOnlySet<string> reads)
    {
        var map = new Dictionary<string, DataValueSnapshot>(StringComparer.Ordinal);
        foreach (var r in reads)
        {
            map[r] = _valueCache.TryGetValue(r, out var v)
                ? v
                : _upstream.ReadTag(r);
        }
        return map;
    }

    private void OnScriptSetVirtualTag(string path, object? value)
    {
        if (!_tags.ContainsKey(path))
        {
            _engineLogger.Warning(
                "Script attempted ctx.SetVirtualTag on non-virtual or non-registered path {Path}", path);
            return;
        }
        var snap = new DataValueSnapshot(value, 0u, _clock(), _clock());
        _valueCache[path] = snap;
        NotifyObservers(path, snap);
        if (_tags[path].Definition.Historize) _history.Record(path, snap);
    }

    private void NotifyObservers(string path, DataValueSnapshot value)
    {
        if (!_observers.TryGetValue(path, out var list)) return;
        Action<string, DataValueSnapshot>[] snapshot;
        lock (list) { snapshot = list.ToArray(); }
        foreach (var obs in snapshot)
        {
            try { obs(path, value); }
            catch (Exception ex)
            {
                _engineLogger.Warning(ex, "Virtual-tag observer for {Path} threw", path);
            }
        }
    }

    private static object? CoerceResult(object? raw, DriverDataType target)
    {
        if (raw is null) return null;
        try
        {
            return target switch
            {
                DriverDataType.Boolean => Convert.ToBoolean(raw),
                DriverDataType.Int32 => Convert.ToInt32(raw),
                DriverDataType.Int64 => Convert.ToInt64(raw),
                DriverDataType.Float32 => Convert.ToSingle(raw),
                DriverDataType.Float64 => Convert.ToDouble(raw),
                DriverDataType.String => Convert.ToString(raw) ?? string.Empty,
                DriverDataType.DateTime => raw is DateTime dt ? dt : Convert.ToDateTime(raw),
                _ => raw,
            };
        }
        catch
        {
            // Caller logs + maps to BadTypeMismatch — we let null propagate so the
            // outer evaluation path sets the Bad quality.
            return null;
        }
    }

    private void UnsubscribeFromUpstream()
    {
        foreach (var s in _upstreamSubscriptions)
        {
            try { s.Dispose(); } catch { /* best effort */ }
        }
        _upstreamSubscriptions.Clear();
    }

    private void EnsureLoaded()
    {
        if (!_loaded) throw new InvalidOperationException(
            "VirtualTagEngine not loaded. Call Load(definitions) first.");
    }

    public void Dispose()
    {
        if (_disposed) return;
        _disposed = true;
        UnsubscribeFromUpstream();
        _tags.Clear();
        _graph.Clear();
    }

    internal DependencyGraph GraphForTesting => _graph;

    private sealed class Unsub : IDisposable
    {
        private readonly VirtualTagEngine _engine;
        private readonly string _path;
        private readonly Action<string, DataValueSnapshot> _observer;
        public Unsub(VirtualTagEngine e, string path, Action<string, DataValueSnapshot> observer)
        {
            _engine = e; _path = path; _observer = observer;
        }
        public void Dispose()
        {
            if (_engine._observers.TryGetValue(_path, out var list))
            {
                lock (list) { list.Remove(_observer); }
            }
        }
    }

    internal sealed record VirtualTagState(
        VirtualTagDefinition Definition,
        TimedScriptEvaluator<VirtualTagContext, object?> Evaluator,
        IReadOnlySet<string> Reads,
        IReadOnlySet<string> Writes,
        ILogger Logger);
}
