using System.Collections.Concurrent;
using System.Text.Json;
using Microsoft.CodeAnalysis.CSharp.Scripting;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Entities.InboundApi;
using ScadaLink.Commons.Types;

namespace ScadaLink.InboundAPI;

/// <summary>
/// WP-3: Executes the C# script associated with an inbound API method.
/// Compiles method scripts via Roslyn and caches compiled delegates.
/// </summary>
public class InboundScriptExecutor
{
    private readonly ILogger<InboundScriptExecutor> _logger;

    // InboundAPI-001: this executor is registered as a singleton and its handler cache
    // is read and written from concurrent ASP.NET request threads. A plain Dictionary is
    // not safe for concurrent read/write, so a ConcurrentDictionary is used throughout.
    private readonly ConcurrentDictionary<string, Func<InboundScriptContext, Task<object?>>> _scriptHandlers = new();

    private readonly IServiceProvider _serviceProvider;

    public InboundScriptExecutor(ILogger<InboundScriptExecutor> logger, IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
    }

    /// <summary>
    /// Registers a compiled script handler for a method name.
    /// </summary>
    public void RegisterHandler(string methodName, Func<InboundScriptContext, Task<object?>> handler)
    {
        _scriptHandlers[methodName] = handler;
    }

    /// <summary>
    /// Removes a compiled script handler for a method name.
    /// </summary>
    public void RemoveHandler(string methodName)
    {
        _scriptHandlers.TryRemove(methodName, out _);
    }

    /// <summary>
    /// Compiles and registers a single API method script. Returns <c>false</c> if the
    /// script is empty, fails Roslyn compilation, or violates the script trust model.
    /// </summary>
    public bool CompileAndRegister(ApiMethod method)
        => Compile(method) is { } handler && Register(method.Name, handler);

    private bool Register(string methodName, Func<InboundScriptContext, Task<object?>> handler)
    {
        _scriptHandlers[methodName] = handler;
        return true;
    }

    /// <summary>
    /// Compiles a single API method script into an executable handler. Returns
    /// <c>null</c> when the script is missing, fails to compile, or violates the
    /// script trust model (InboundAPI-005). Does not mutate the handler cache.
    /// </summary>
    private Func<InboundScriptContext, Task<object?>>? Compile(ApiMethod method)
    {
        if (string.IsNullOrWhiteSpace(method.Script))
        {
            _logger.LogWarning("API method {Method} has no script code", method.Name);
            return null;
        }

        // InboundAPI-005: enforce the script trust model before compiling. Roslyn
        // scripting performs no API allow/deny-listing, so forbidden namespaces must
        // be rejected statically or the script could reach the host process.
        var violations = ForbiddenApiChecker.FindViolations(method.Script);
        if (violations.Count > 0)
        {
            _logger.LogWarning(
                "API method {Method} script rejected — trust model violation(s): {Violations}",
                method.Name, string.Join("; ", violations));
            return null;
        }

        try
        {
            var scriptOptions = ScriptOptions.Default
                .WithReferences(
                    typeof(object).Assembly,
                    typeof(Enumerable).Assembly,
                    typeof(Dictionary<,>).Assembly,
                    typeof(RouteHelper).Assembly,
                    typeof(ScriptParameters).Assembly,
                    typeof(Microsoft.CSharp.RuntimeBinder.CSharpArgumentInfo).Assembly)
                .WithImports(
                    "System",
                    "System.Collections.Generic",
                    "System.Linq",
                    "System.Threading.Tasks");

            var compiled = CSharpScript.Create<object?>(
                method.Script,
                scriptOptions,
                globalsType: typeof(InboundScriptContext));

            var diagnostics = compiled.Compile();
            var errors = diagnostics
                .Where(d => d.Severity == Microsoft.CodeAnalysis.DiagnosticSeverity.Error)
                .Select(d => d.GetMessage())
                .ToList();

            if (errors.Count > 0)
            {
                _logger.LogWarning(
                    "API method {Method} script compilation failed: {Errors}",
                    method.Name, string.Join("; ", errors));
                return null;
            }

            _logger.LogInformation("API method {Method} script compiled", method.Name);
            return async ctx =>
            {
                var state = await compiled.RunAsync(ctx, ctx.CancellationToken);
                return state.ReturnValue;
            };
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to compile API method {Method} script", method.Name);
            return null;
        }
    }

    /// <summary>
    /// Executes the script for the given method with the provided context.
    /// </summary>
    public async Task<InboundScriptResult> ExecuteAsync(
        ApiMethod method,
        IReadOnlyDictionary<string, object?> parameters,
        RouteHelper route,
        TimeSpan timeout,
        CancellationToken cancellationToken = default)
    {
        // InboundAPI-004: keep the timeout source and the request-abort source
        // separable. A single linked CTS makes a genuine client disconnect
        // indistinguishable from a method timeout, so a normal disconnect would be
        // logged and reported as "Script execution timed out". Use a dedicated
        // timeout CTS, linked with the request token, so the two can be told apart.
        using var timeoutCts = new CancellationTokenSource(timeout);
        using var cts = CancellationTokenSource.CreateLinkedTokenSource(
            cancellationToken, timeoutCts.Token);

        try
        {
            var context = new InboundScriptContext(parameters, route, cts.Token);

            if (!_scriptHandlers.TryGetValue(method.Name, out var handler))
            {
                // Lazy compile on first request (handles methods created after startup).
                // Compile outside the cache so a failed compile is not stored, then add
                // atomically so concurrent first-callers share a single handler instance.
                var compiled = Compile(method);
                if (compiled == null)
                    return new InboundScriptResult(false, null, "Script compilation failed for this method");
                handler = _scriptHandlers.GetOrAdd(method.Name, compiled);
            }

            var result = await handler(context).WaitAsync(cts.Token);

            var resultJson = result != null
                ? JsonSerializer.Serialize(result)
                : null;

            return new InboundScriptResult(true, resultJson, null);
        }
        catch (OperationCanceledException)
        {
            // InboundAPI-004: distinguish a genuine method timeout from a client
            // abort. Only the timeout CTS firing is a real timeout; if the caller's
            // request token fired, the client disconnected — do not pollute the
            // timeout log (reserved for genuine script-execution timeouts).
            if (timeoutCts.IsCancellationRequested && !cancellationToken.IsCancellationRequested)
            {
                _logger.LogWarning("Script execution timed out for method {Method}", method.Name);
                return new InboundScriptResult(false, null, "Script execution timed out");
            }

            _logger.LogDebug("Inbound API request for method {Method} cancelled by client", method.Name);
            return new InboundScriptResult(false, null, "Request cancelled by client");
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Script execution failed for method {Method}", method.Name);
            // WP-5: Safe error message, no internal details
            return new InboundScriptResult(false, null, "Internal script error");
        }
    }
}

/// <summary>
/// Context provided to inbound API scripts.
/// </summary>
public class InboundScriptContext
{
    public ScriptParameters Parameters { get; }
    public RouteHelper Route { get; }
    public CancellationToken CancellationToken { get; }

    public InboundScriptContext(
        IReadOnlyDictionary<string, object?> parameters,
        RouteHelper route,
        CancellationToken cancellationToken = default)
    {
        Parameters = new ScriptParameters(parameters);
        Route = route;
        CancellationToken = cancellationToken;
    }
}

/// <summary>
/// Result of executing an inbound API script.
/// </summary>
public record InboundScriptResult(
    bool Success,
    string? ResultJson,
    string? ErrorMessage);
