using System.Text;

namespace ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis;

/// <summary>
/// Per-call console capture for the Test Run sandbox.
/// <para>
/// Sandbox scripts use <c>System.Console.WriteLine</c> for ad-hoc output. The
/// sandbox needs to capture that output per execution. <c>Console.Out</c> is,
/// however, <b>process-global</b>: redirecting it with <c>Console.SetOut</c> for
/// the duration of one run corrupts any other run executing concurrently —
/// outputs interleave, and whichever run finishes first restores
/// <c>Console.Out</c> while the others are still writing.
/// </para>
/// <para>
/// This writer is installed into <c>Console.Out</c>/<c>Console.Error</c>
/// <b>exactly once</b> (see <see cref="Install"/>) and never removed. Each
/// concurrent run pushes its own buffer onto an <see cref="AsyncLocal{T}"/>
/// scope via <see cref="BeginCapture"/>; writes on that run's logical call-tree
/// land in that run's buffer only. Writes made on threads with no active
/// capture scope (i.e. genuine host-process console output) fall through to the
/// original writer. No process-global mutation happens per run.
/// </para>
/// </summary>
internal sealed class SandboxConsoleCapture : TextWriter
{
    private static readonly object InstallLock = new();
    private static SandboxConsoleCapture? _outInstance;
    private static SandboxConsoleCapture? _errorInstance;

    private readonly TextWriter _fallback;
    private readonly AsyncLocal<StringWriter?> _current = new();

    private SandboxConsoleCapture(TextWriter fallback) => _fallback = fallback;

    /// <inheritdoc />
    public override Encoding Encoding => _fallback.Encoding;

    /// <summary>
    /// Installs the routing writers into <see cref="Console.Out"/> and
    /// <see cref="Console.Error"/> once for the process. Idempotent and
    /// thread-safe. Subsequent calls return the already-installed instances.
    /// </summary>
    /// <returns>The installed <see cref="SandboxConsoleCapture"/> instances for stdout and stderr.</returns>
    public static (SandboxConsoleCapture Out, SandboxConsoleCapture Error) Install()
    {
        if (_outInstance != null && _errorInstance != null)
            return (_outInstance, _errorInstance);

        lock (InstallLock)
        {
            if (_outInstance == null)
            {
                _outInstance = new SandboxConsoleCapture(Console.Out);
                Console.SetOut(_outInstance);
            }

            if (_errorInstance == null)
            {
                _errorInstance = new SandboxConsoleCapture(Console.Error);
                Console.SetError(_errorInstance);
            }
        }

        return (_outInstance, _errorInstance);
    }

    /// <summary>
    /// Begins a capture scope on the current logical (async) call-tree. All
    /// console writes from this point until the returned scope is disposed are
    /// routed into <paramref name="buffer"/> instead of the original writer.
    /// The scope is restored on dispose, so nesting and concurrent scopes on
    /// other call-trees are unaffected.
    /// </summary>
    /// <param name="buffer">The writer that receives console output for this scope.</param>
    /// <returns>A <see cref="CaptureScope"/> that, when disposed, restores the previous capture state.</returns>
    public CaptureScope BeginCapture(StringWriter buffer)
    {
        var previous = _current.Value;
        _current.Value = buffer;
        return new CaptureScope(this, previous);
    }

    // Intra-script concurrency hardening. A sandboxed script
    // can fan out work with `Task.WhenAll` / `Task.Run`; `AsyncLocal` flows
    // the capture `StringWriter` into every child task, so two tasks can
    // race the *same* buffer. `StringWriter` is not thread-safe — concurrent
    // `Write`/`WriteLine` calls can corrupt the underlying `StringBuilder`
    // and either throw or interleave at the character level. We lock on the
    // captured writer itself so writes from one capture scope serialise;
    // fall-through to the original `_fallback` (host-process console) is
    // unlocked because the BCL's process-wide `Console.Out` is already
    // synchronised by its TextWriter wrapper.
    /// <inheritdoc />
    public override void Write(char value) => WriteSynchronized(t => t.Write(value));

    /// <inheritdoc />
    public override void Write(string? value) => WriteSynchronized(t => t.Write(value));

    /// <inheritdoc />
    public override void Write(char[] buffer, int index, int count) =>
        WriteSynchronized(t => t.Write(buffer, index, count));

    /// <inheritdoc />
    public override void WriteLine() => WriteSynchronized(t => t.WriteLine());

    /// <inheritdoc />
    public override void WriteLine(string? value) => WriteSynchronized(t => t.WriteLine(value));

    /// <summary>
    /// Routes a single write through the currently-active capture buffer
    /// under a lock on that buffer, or to the unwrapped fallback writer when
    /// no capture scope is active. The lock target is the `StringWriter`
    /// instance itself — different capture scopes have different writers,
    /// so two unrelated scopes never block each other.
    /// </summary>
    private void WriteSynchronized(Action<TextWriter> write)
    {
        var captured = _current.Value;
        if (captured is null)
        {
            write(_fallback);
            return;
        }

        lock (captured)
        {
            write(captured);
        }
    }

    internal readonly struct CaptureScope : IDisposable
    {
        private readonly SandboxConsoleCapture _owner;
        private readonly StringWriter? _previous;

        /// <summary>
        /// Initializes a capture scope that restores the previous writer on dispose.
        /// </summary>
        /// <param name="owner">The owning <see cref="SandboxConsoleCapture"/> instance.</param>
        /// <param name="previous">The writer that was active before this scope was opened.</param>
        internal CaptureScope(SandboxConsoleCapture owner, StringWriter? previous)
        {
            _owner = owner;
            _previous = previous;
        }

        /// <summary>Restores the previous capture scope.</summary>
        public void Dispose() => _owner._current.Value = _previous;
    }
}
