using Akka.Actor;
using Microsoft.CodeAnalysis.Scripting;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Messages.ScriptExecution;
using ScadaLink.Commons.Messages.Streaming;
using ScadaLink.Commons.Types.Flattening;
using ScadaLink.HealthMonitoring;
using ScadaLink.SiteEventLogging;
using ScadaLink.SiteRuntime.Scripts;
using System.Text.Json;

namespace ScadaLink.SiteRuntime.Actors;

/// <summary>
/// WP-15: Script Actor — coordinator actor, child of Instance Actor.
/// Holds compiled script delegate, manages trigger configuration, and spawns
/// ScriptExecutionActor children per invocation. Does not block on child completion.
///
/// Trigger types:
///   - Interval: uses Akka timers to fire periodically
///   - ValueChange: receives attribute change notifications from Instance Actor
///   - Conditional: evaluates a condition on attribute change
///
/// Supervision strategy: Resume on exception (coordinator preserves state).
/// </summary>
public class ScriptActor : ReceiveActor, IWithTimers
{
    private readonly string _scriptName;
    private readonly string _instanceName;
    private readonly IActorRef _instanceActor;
    private readonly SharedScriptLibrary _sharedScriptLibrary;
    private readonly SiteRuntimeOptions _options;
    private readonly ILogger _logger;
    private readonly ISiteHealthCollector? _healthCollector;
    private readonly IServiceProvider? _serviceProvider;

    private Script<object?>? _compiledScript;
    private ScriptTriggerConfig? _triggerConfig;
    private TimeSpan? _minTimeBetweenRuns;
    private DateTimeOffset _lastExecutionTime = DateTimeOffset.MinValue;
    private int _executionCounter;
    private readonly Commons.Types.Scripts.ScriptScope _scope;

    // Expression trigger state: compiled expression, edge-tracking, and the
    // attribute snapshot the expression evaluates against.
    private readonly Script<object?>? _compiledTriggerExpression;
    private bool _lastExpressionResult;
    private readonly Dictionary<string, object?> _attributeSnapshot = new();

    public ITimerScheduler Timers { get; set; } = null!;

    public ScriptActor(
        string scriptName,
        string instanceName,
        IActorRef instanceActor,
        Script<object?>? compiledScript,
        ResolvedScript scriptConfig,
        SharedScriptLibrary sharedScriptLibrary,
        SiteRuntimeOptions options,
        ILogger logger,
        Script<object?>? compiledTriggerExpression = null,
        IReadOnlyDictionary<string, object?>? initialAttributes = null,
        ISiteHealthCollector? healthCollector = null,
        IServiceProvider? serviceProvider = null)
    {
        _scriptName = scriptName;
        _instanceName = instanceName;
        _instanceActor = instanceActor;
        _compiledScript = compiledScript;
        _sharedScriptLibrary = sharedScriptLibrary;
        _options = options;
        _logger = logger;
        _healthCollector = healthCollector;
        _serviceProvider = serviceProvider;
        _minTimeBetweenRuns = scriptConfig.MinTimeBetweenRuns;
        _scope = scriptConfig.Scope;
        _compiledTriggerExpression = compiledTriggerExpression;

        // Seed the trigger-expression attribute snapshot from the instance's
        // initial attribute set so static attributes (which never re-emit an
        // AttributeValueChanged after deploy) evaluate correctly at startup.
        if (initialAttributes != null)
        {
            foreach (var kvp in initialAttributes)
                _attributeSnapshot[kvp.Key] = kvp.Value;
        }

        // Parse trigger configuration
        _triggerConfig = ParseTriggerConfig(scriptConfig.TriggerType, scriptConfig.TriggerConfiguration);

        // Handle script call requests (Ask pattern from Instance Actor or ScriptRuntimeContext)
        Receive<ScriptCallRequest>(HandleScriptCallRequest);

        // Handle attribute value changes for value-change and conditional triggers
        Receive<AttributeValueChanged>(HandleAttributeValueChanged);

        // Handle interval tick
        Receive<IntervalTick>(_ => TrySpawnExecution(null));

        // Handle execution completion (for logging/metrics)
        Receive<ScriptExecutionCompleted>(HandleExecutionCompleted);
    }

    protected override void PreStart()
    {
        base.PreStart();

        // Set up interval trigger if configured
        if (_triggerConfig is IntervalTriggerConfig interval)
        {
            Timers.StartPeriodicTimer(
                "interval-trigger",
                IntervalTick.Instance,
                interval.Interval,
                interval.Interval);

            _logger.LogDebug(
                "ScriptActor {Script} on {Instance}: interval trigger set to {Interval}",
                _scriptName, _instanceName, interval.Interval);
        }

        _logger.LogInformation(
            "ScriptActor {Script} started on instance {Instance}",
            _scriptName, _instanceName);
    }

    /// <summary>
    /// Supervision: Resume on exception — coordinator preserves state.
    /// ScriptExecutionActors are stopped on unhandled exceptions.
    /// </summary>
    protected override SupervisorStrategy SupervisorStrategy()
    {
        return new OneForOneStrategy(
            maxNrOfRetries: -1,
            withinTimeRange: TimeSpan.FromMinutes(1),
            decider: Decider.From(ex =>
            {
                _logger.LogWarning(ex,
                    "ScriptExecutionActor for {Script} on {Instance} failed, stopping",
                    _scriptName, _instanceName);
                return Directive.Stop;
            }));
    }

    /// <summary>
    /// Handles CallScript ask from ScriptRuntimeContext or Instance Actor.
    /// Spawns a ScriptExecutionActor and forwards the sender for reply.
    /// </summary>
    private void HandleScriptCallRequest(ScriptCallRequest request)
    {
        if (_compiledScript == null)
        {
            Sender.Tell(new ScriptCallResult(
                request.CorrelationId,
                false,
                null,
                $"Script '{_scriptName}' is not compiled."));
            return;
        }

        SpawnExecution(request.Parameters, request.CurrentCallDepth, Sender, request.CorrelationId);
    }

    /// <summary>
    /// Handles attribute value changes — triggers script if configured for
    /// value-change, conditional, or expression. The attribute snapshot is
    /// updated for every change before any trigger logic runs.
    /// </summary>
    private void HandleAttributeValueChanged(AttributeValueChanged changed)
    {
        // Keep the snapshot current for every change, regardless of trigger type.
        _attributeSnapshot[changed.AttributeName] = changed.Value;

        if (_triggerConfig is ValueChangeTriggerConfig valueTrigger)
        {
            if (valueTrigger.AttributeName == changed.AttributeName)
            {
                TrySpawnExecution(null);
            }
        }
        else if (_triggerConfig is ConditionalTriggerConfig conditional)
        {
            if (conditional.AttributeName == changed.AttributeName)
            {
                // Evaluate condition
                if (EvaluateCondition(conditional, changed.Value))
                {
                    TrySpawnExecution(null);
                }
            }
        }
        else if (_triggerConfig is ExpressionTriggerConfig)
        {
            EvaluateExpressionTrigger();
        }
    }

    /// <summary>
    /// Evaluates the compiled trigger expression against the current attribute
    /// snapshot and runs the script edge-triggered — once per false→true
    /// transition. A throwing or non-bool expression is treated as false and
    /// logged as a script error; the actor never crashes.
    /// </summary>
    private void EvaluateExpressionTrigger()
    {
        if (_compiledTriggerExpression == null) return;

        bool result;
        try
        {
            var globals = new TriggerExpressionGlobals(_attributeSnapshot);
            // Bound evaluation with a short timeout. The CancellationToken
            // covers cooperative/async cases; a pathological CPU-bound
            // expression is not fully interruptible. Acceptable because
            // trigger expressions are authored by trusted Design-role users
            // and are compile-checked pre-deployment.
            using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(2));
            var state = _compiledTriggerExpression
                .RunAsync(globals, cancellationToken: cts.Token)
                .GetAwaiter().GetResult();
            result = state.ReturnValue is bool b && b;
        }
        catch (Exception ex)
        {
            // OperationCanceledException (timeout) falls through here too,
            // and is correctly treated as false.
            LogExpressionError(ex);
            result = false;
        }

        if (result && !_lastExpressionResult)
        {
            TrySpawnExecution(null);
        }

        _lastExpressionResult = result;
    }

    /// <summary>
    /// Records a trigger-expression evaluation failure to the site event log,
    /// mirroring how ScriptExecutionActor reports script errors.
    /// </summary>
    private void LogExpressionError(Exception ex)
    {
        _healthCollector?.IncrementScriptError();
        var errorMsg = $"Trigger expression for script '{_scriptName}' on instance '{_instanceName}' failed: {ex.Message}";
        _logger.LogError(ex, "Trigger expression evaluation failed: {Script} on {Instance}", _scriptName, _instanceName);

        _ = _serviceProvider?.GetService<ISiteEventLogger>()?.LogEventAsync(
            "script", "Error", _instanceName, $"ScriptActor:{_scriptName}", errorMsg, ex.ToString());
    }

    /// <summary>
    /// Attempts to spawn a script execution, respecting MinTimeBetweenRuns.
    /// </summary>
    private void TrySpawnExecution(IReadOnlyDictionary<string, object?>? parameters)
    {
        if (_compiledScript == null) return;

        if (_minTimeBetweenRuns.HasValue)
        {
            var elapsed = DateTimeOffset.UtcNow - _lastExecutionTime;
            if (elapsed < _minTimeBetweenRuns.Value)
            {
                _logger.LogDebug(
                    "Script {Script} on {Instance}: skipping execution, min time between runs not elapsed ({Elapsed} < {Min})",
                    _scriptName, _instanceName, elapsed, _minTimeBetweenRuns.Value);
                return;
            }
        }

        _lastExecutionTime = DateTimeOffset.UtcNow;
        SpawnExecution(parameters, 0, ActorRefs.NoSender!, Guid.NewGuid().ToString());
    }

    /// <summary>
    /// Spawns a new ScriptExecutionActor child for this invocation.
    /// Multiple concurrent executions are allowed.
    /// </summary>
    private void SpawnExecution(
        IReadOnlyDictionary<string, object?>? parameters,
        int callDepth,
        IActorRef replyTo,
        string correlationId)
    {
        var executionId = $"{_scriptName}-exec-{_executionCounter++}";

        // NOTE: In production, configure a dedicated blocking I/O dispatcher via HOCON:
        //   akka.actor.script-execution-dispatcher { type = PinnedDispatcher }
        // and chain .WithDispatcher("akka.actor.script-execution-dispatcher") below.
        var props = Props.Create(() => new ScriptExecutionActor(
            _scriptName,
            _instanceName,
            _compiledScript!,
            parameters,
            callDepth,
            _instanceActor,
            _sharedScriptLibrary,
            _options,
            replyTo,
            correlationId,
            _logger,
            _scope,
            _healthCollector,
            _serviceProvider));

        Context.ActorOf(props, executionId);
    }

    private void HandleExecutionCompleted(ScriptExecutionCompleted msg)
    {
        _logger.LogDebug(
            "Script {Script} execution completed on {Instance}: success={Success}",
            _scriptName, _instanceName, msg.Success);
    }

    private static bool EvaluateCondition(ConditionalTriggerConfig config, object? value)
    {
        if (value == null) return false;

        try
        {
            var numericValue = Convert.ToDouble(value);
            return config.Operator switch
            {
                ">" => numericValue > config.Threshold,
                ">=" => numericValue >= config.Threshold,
                "<" => numericValue < config.Threshold,
                "<=" => numericValue <= config.Threshold,
                "==" => Math.Abs(numericValue - config.Threshold) < 0.0001,
                "!=" => Math.Abs(numericValue - config.Threshold) >= 0.0001,
                _ => false
            };
        }
        catch
        {
            return string.Equals(value.ToString(), config.Threshold.ToString(), StringComparison.Ordinal);
        }
    }

    private static ScriptTriggerConfig? ParseTriggerConfig(string? triggerType, string? triggerConfigJson)
    {
        if (string.IsNullOrEmpty(triggerType)) return null;

        return triggerType.ToLowerInvariant() switch
        {
            "interval" => ParseIntervalTrigger(triggerConfigJson),
            "valuechange" => ParseValueChangeTrigger(triggerConfigJson),
            "conditional" => ParseConditionalTrigger(triggerConfigJson),
            "expression" => ParseExpressionTrigger(triggerConfigJson),
            "call" => null, // No automatic trigger — invoked only via Instance.CallScript()
            _ => null
        };
    }

    private static ExpressionTriggerConfig? ParseExpressionTrigger(string? json)
    {
        var expr = TriggerExpressionGlobals.ExtractExpression(json);
        return expr == null ? null : new ExpressionTriggerConfig(expr);
    }

    private static IntervalTriggerConfig? ParseIntervalTrigger(string? json)
    {
        if (string.IsNullOrEmpty(json)) return null;
        try
        {
            var doc = JsonDocument.Parse(json);
            var ms = doc.RootElement.GetProperty("intervalMs").GetInt64();
            return new IntervalTriggerConfig(TimeSpan.FromMilliseconds(ms));
        }
        catch { return null; }
    }

    private static ValueChangeTriggerConfig? ParseValueChangeTrigger(string? json)
    {
        if (string.IsNullOrEmpty(json)) return null;
        try
        {
            var doc = JsonDocument.Parse(json);
            var attr = doc.RootElement.GetProperty("attributeName").GetString()!;
            return new ValueChangeTriggerConfig(attr);
        }
        catch { return null; }
    }

    private static ConditionalTriggerConfig? ParseConditionalTrigger(string? json)
    {
        if (string.IsNullOrEmpty(json)) return null;
        try
        {
            var doc = JsonDocument.Parse(json);
            var attr = doc.RootElement.GetProperty("attributeName").GetString()!;
            var op = doc.RootElement.GetProperty("operator").GetString()!;
            var threshold = doc.RootElement.GetProperty("threshold").GetDouble();
            return new ConditionalTriggerConfig(attr, op, threshold);
        }
        catch { return null; }
    }

    // ── Internal messages ──

    internal sealed class IntervalTick
    {
        public static readonly IntervalTick Instance = new();
        private IntervalTick() { }
    }

    internal record ScriptExecutionCompleted(string ScriptName, bool Success, string? Error);
}

// ── Trigger config types ──

internal record IntervalTriggerConfig(TimeSpan Interval) : ScriptTriggerConfig;
internal record ValueChangeTriggerConfig(string AttributeName) : ScriptTriggerConfig;
internal record ConditionalTriggerConfig(string AttributeName, string Operator, double Threshold) : ScriptTriggerConfig;
internal record ExpressionTriggerConfig(string Expression) : ScriptTriggerConfig;
internal abstract record ScriptTriggerConfig;
