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;
    private readonly Dictionary<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.Remove(methodName);
    }

    /// <summary>
    /// Compiles and registers a single API method script.
    /// </summary>
    public bool CompileAndRegister(ApiMethod method)
    {
        if (string.IsNullOrWhiteSpace(method.Script))
        {
            _logger.LogWarning("API method {Method} has no script code", method.Name);
            return false;
        }

        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 false;
            }

            _scriptHandlers[method.Name] = async ctx =>
            {
                var state = await compiled.RunAsync(ctx, ctx.CancellationToken);
                return state.ReturnValue;
            };

            _logger.LogInformation("API method {Method} script compiled and registered", method.Name);
            return true;
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to compile API method {Method} script", method.Name);
            return false;
        }
    }

    /// <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)
    {
        try
        {
            using var cts = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
            cts.CancelAfter(timeout);

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

            object? result;
            if (!_scriptHandlers.TryGetValue(method.Name, out var handler))
            {
                // Lazy compile on first request (handles methods created after startup)
                if (!CompileAndRegister(method))
                    return new InboundScriptResult(false, null, "Script compilation failed for this method");
                handler = _scriptHandlers[method.Name];
            }
            result = await handler(context).WaitAsync(cts.Token);

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

            return new InboundScriptResult(true, resultJson, null);
        }
        catch (OperationCanceledException)
        {
            _logger.LogWarning("Script execution timed out for method {Method}", method.Name);
            return new InboundScriptResult(false, null, "Script execution timed out");
        }
        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);
