using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Logging;
using ZB.MOM.WW.ScadaBridge.Commons.Types;

namespace ZB.MOM.WW.ScadaBridge.SiteRuntime.Scripts;

/// <summary>
/// Shared Script Library — stores compiled shared script delegates in memory.
/// Shared scripts are compiled when received from central and executed inline
/// (direct method call, not actor message). NOT available on central.
/// Recompiled on update when new artifacts arrive.
/// </summary>
public class SharedScriptLibrary
{
    private readonly ScriptCompilationService _compilationService;
    private readonly ILogger<SharedScriptLibrary> _logger;
    private readonly Dictionary<string, Script<object?>> _compiledScripts = new();
    private readonly object _lock = new();

    /// <summary>
    /// Initializes a new <see cref="SharedScriptLibrary"/> with the compilation service and logger.
    /// </summary>
    /// <param name="compilationService">Service used to compile Roslyn scripts.</param>
    /// <param name="logger">Logger for compilation warnings and diagnostics.</param>
    public SharedScriptLibrary(
        ScriptCompilationService compilationService,
        ILogger<SharedScriptLibrary> logger)
    {
        _compilationService = compilationService;
        _logger = logger;
    }

    /// <summary>
    /// Compiles and registers a shared script. Replaces any existing script with the same name.
    /// Returns true if compilation succeeded, false otherwise.
    /// </summary>
    /// <param name="name">Unique name for the shared script.</param>
    /// <param name="code">C# source code of the shared script.</param>
    /// <returns><c>true</c> if compilation succeeded and the script was registered; <c>false</c> if compilation failed.</returns>
    public bool CompileAndRegister(string name, string code)
    {
        var result = _compilationService.Compile(name, code);
        if (!result.IsSuccess)
        {
            _logger.LogWarning(
                "Shared script '{Name}' failed to compile: {Errors}",
                name, string.Join("; ", result.Errors));
            return false;
        }

        lock (_lock)
        {
            _compiledScripts[name] = result.CompiledScript!;
        }

        _logger.LogInformation("Shared script '{Name}' compiled and registered", name);
        return true;
    }

    /// <summary>
    /// Removes a shared script from the library.
    /// </summary>
    /// <param name="name">Name of the shared script to remove.</param>
    /// <returns><c>true</c> if the script was found and removed; <c>false</c> if no script with that name was registered.</returns>
    public bool Remove(string name)
    {
        lock (_lock)
        {
            return _compiledScripts.Remove(name);
        }
    }

    /// <summary>
    /// Executes a shared script inline with the given runtime context.
    /// This is a direct method call, not an actor message — executes on the calling thread.
    /// </summary>
    /// <param name="scriptName">Name of the shared script to execute.</param>
    /// <param name="context">Runtime context providing instance state and services to the script globals.</param>
    /// <param name="parameters">Optional input parameters passed to the script.</param>
    /// <param name="cancellationToken">Cancellation token for the execution.</param>
    /// <returns>A task that resolves to the script's return value, or <c>null</c> if the script returns nothing.</returns>
    public async Task<object?> ExecuteAsync(
        string scriptName,
        ScriptRuntimeContext context,
        IReadOnlyDictionary<string, object?>? parameters = null,
        CancellationToken cancellationToken = default)
    {
        Script<object?> script;
        lock (_lock)
        {
            if (!_compiledScripts.TryGetValue(scriptName, out script!))
            {
                throw new InvalidOperationException(
                    $"Shared script '{scriptName}' not found in library.");
            }
        }

        var globals = new ScriptGlobals
        {
            Instance = context,
            Parameters = new ScriptParameters(parameters ?? new Dictionary<string, object?>()),
            CancellationToken = cancellationToken
        };

        var state = await script.RunAsync(globals, cancellationToken);
        return state.ReturnValue;
    }

    /// <summary>
    /// Returns the names of all currently registered shared scripts.
    /// </summary>
    /// <returns>A snapshot list of all registered script names at the time of the call.</returns>
    public IReadOnlyList<string> GetRegisteredScriptNames()
    {
        lock (_lock)
        {
            return _compiledScripts.Keys.ToList();
        }
    }

    /// <summary>
    /// Returns whether a script with the given name is registered.
    /// </summary>
    /// <param name="name">Name of the shared script to look up.</param>
    /// <returns><c>true</c> if a script with the given name is registered; otherwise <c>false</c>.</returns>
    public bool Contains(string name)
    {
        lock (_lock)
        {
            return _compiledScripts.ContainsKey(name);
        }
    }
}
