using Akka.Actor;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Messages.ScriptExecution;
using ScadaLink.Commons.Types;
using ScadaLink.HealthMonitoring;
using ScadaLink.SiteEventLogging;
using ScadaLink.SiteRuntime.Scripts;

namespace ScadaLink.SiteRuntime.Actors;

/// <summary>
/// WP-15: Script Execution Actor -- short-lived child of Script Actor.
/// Receives compiled code, params, Instance Actor ref, and call depth.
/// Runs on a dedicated blocking I/O dispatcher.
/// Executes the script via Script Runtime API, returns result, then stops.
///
/// WP-32: Script failures are logged but do not disable the script.
/// Supervision: Stop on unhandled exception (parent ScriptActor decides).
/// </summary>
public class ScriptExecutionActor : ReceiveActor
{
    public ScriptExecutionActor(
        string scriptName,
        string instanceName,
        Script<object?> compiledScript,
        IReadOnlyDictionary<string, object?>? parameters,
        int callDepth,
        IActorRef instanceActor,
        SharedScriptLibrary sharedScriptLibrary,
        SiteRuntimeOptions options,
        IActorRef replyTo,
        string correlationId,
        ILogger logger,
        Commons.Types.Scripts.ScriptScope scope,
        ISiteHealthCollector? healthCollector = null,
        IServiceProvider? serviceProvider = null)
    {
        // Immediately begin execution
        var self = Self;
        var parent = Context.Parent;

        ExecuteScript(
            scriptName, instanceName, compiledScript, parameters, callDepth,
            instanceActor, sharedScriptLibrary, options, replyTo, correlationId,
            self, parent, logger, scope, healthCollector, serviceProvider);
    }

    private static void ExecuteScript(
        string scriptName,
        string instanceName,
        Script<object?> compiledScript,
        IReadOnlyDictionary<string, object?>? parameters,
        int callDepth,
        IActorRef instanceActor,
        SharedScriptLibrary sharedScriptLibrary,
        SiteRuntimeOptions options,
        IActorRef replyTo,
        string correlationId,
        IActorRef self,
        IActorRef parent,
        ILogger logger,
        Commons.Types.Scripts.ScriptScope scope,
        ISiteHealthCollector? healthCollector,
        IServiceProvider? serviceProvider)
    {
        var timeout = TimeSpan.FromSeconds(options.ScriptExecutionTimeoutSeconds);

        // CTS must be created inside the async lambda so it outlives this method
        _ = Task.Run(async () =>
        {
            IServiceScope? serviceScope = null;
            // ISiteEventLogger is a singleton; resolve from the root provider so
            // it is available to the catch blocks regardless of scope state.
            var siteEventLogger = serviceProvider?.GetService<ISiteEventLogger>();
            using var cts = new CancellationTokenSource(timeout);
            try
            {
                // Resolve integration services from DI (scoped lifetime)
                IExternalSystemClient? externalSystemClient = null;
                IDatabaseGateway? databaseGateway = null;
                INotificationDeliveryService? notificationService = null;

                if (serviceProvider != null)
                {
                    serviceScope = serviceProvider.CreateScope();
                    externalSystemClient = serviceScope.ServiceProvider.GetService<IExternalSystemClient>();
                    databaseGateway = serviceScope.ServiceProvider.GetService<IDatabaseGateway>();
                    notificationService = serviceScope.ServiceProvider.GetService<INotificationDeliveryService>();
                }

                var context = new ScriptRuntimeContext(
                    instanceActor,
                    self,
                    sharedScriptLibrary,
                    callDepth,
                    options.MaxScriptCallDepth,
                    timeout,
                    instanceName,
                    logger,
                    externalSystemClient,
                    databaseGateway,
                    notificationService);

                var globals = new ScriptGlobals
                {
                    Instance = context,
                    Parameters = new ScriptParameters(parameters ?? new Dictionary<string, object?>()),
                    CancellationToken = cts.Token,
                    Scope = scope
                };

                var state = await compiledScript.RunAsync(globals, cts.Token);

                // Send result to requester if this was an Ask-based call
                if (!replyTo.IsNobody())
                {
                    replyTo.Tell(new ScriptCallResult(correlationId, true, state.ReturnValue, null));
                }

                // Notify parent of completion
                parent.Tell(new ScriptActor.ScriptExecutionCompleted(scriptName, true, null));
            }
            catch (OperationCanceledException)
            {
                healthCollector?.IncrementScriptError();
                var errorMsg = $"Script '{scriptName}' on instance '{instanceName}' timed out after {timeout.TotalSeconds}s";
                logger.LogWarning(errorMsg);

                // WP-32: Failures recorded to site event log; script NOT disabled after failure.
                _ = siteEventLogger?.LogEventAsync(
                    "script", "Error", instanceName, $"ScriptActor:{scriptName}", errorMsg);

                if (!replyTo.IsNobody())
                {
                    replyTo.Tell(new ScriptCallResult(correlationId, false, null, errorMsg));
                }

                parent.Tell(new ScriptActor.ScriptExecutionCompleted(scriptName, false, errorMsg));
            }
            catch (Exception ex)
            {
                healthCollector?.IncrementScriptError();
                // WP-32: Failures recorded to site event log; script NOT disabled after failure.
                var errorMsg = $"Script '{scriptName}' on instance '{instanceName}' failed: {ex.Message}";
                logger.LogError(ex, "Script execution failed: {Script} on {Instance}", scriptName, instanceName);

                _ = siteEventLogger?.LogEventAsync(
                    "script", "Error", instanceName, $"ScriptActor:{scriptName}", errorMsg, ex.ToString());

                if (!replyTo.IsNobody())
                {
                    replyTo.Tell(new ScriptCallResult(correlationId, false, null, errorMsg));
                }

                parent.Tell(new ScriptActor.ScriptExecutionCompleted(scriptName, false, errorMsg));
            }
            finally
            {
                // Dispose the DI scope (and scoped services) after script execution completes
                serviceScope?.Dispose();
                // Stop self after execution completes
                self.Tell(PoisonPill.Instance);
            }
        });
    }
}
