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

namespace ZB.MOM.WW.ScadaBridge.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 threshold comparison on attribute change
///   - Expression: evaluates a compiled boolean expression on attribute change
/// Conditional and Expression triggers carry a <see cref="TriggerMode"/>:
/// OnTrue fires as the condition becomes true; WhileTrue additionally re-fires
/// on a timer (cadence = MinTimeBetweenRuns) while the condition stays true.
///
/// 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;

    /// <summary>
    /// M2.5 (#9): the per-script execution timeout in seconds, or null to use the
    /// global default. Threaded down to each spawned <see cref="ScriptExecutionActor"/>,
    /// which applies <c>perScript ?? global</c> (and treats ≤ 0 as "use global").
    /// </summary>
    private readonly int? _executionTimeoutSeconds;
    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();

    // WhileTrue trigger state: the most recent truth value of a Conditional
    // trigger's comparison, used to detect false->true / true->false edges.
    // (Expression triggers reuse _lastExpressionResult for the same purpose.)
    private bool _conditionState;

    /// <summary>Timer key for the WhileTrue re-fire timer (cadence = MinTimeBetweenRuns).</summary>
    private const string WhileTrueTimerKey = "whiletrue-trigger";

    /// <summary>
    /// SiteRuntime-017: the exact dictionary instance this actor was seeded from
    /// at construction. The Instance Actor must pass a private snapshot here, not
    /// its live <c>_attributes</c> field — sharing the live dictionary lets this
    /// constructor enumerate it while the Instance Actor mutates it on another
    /// thread. Exposed for regression coverage of that isolation contract.
    /// </summary>
    internal IReadOnlyDictionary<string, object?>? SeedAttributesReference { get; }

    /// <summary>Gets or sets the Akka timer scheduler used to schedule interval and WhileTrue triggers.</summary>
    public ITimerScheduler Timers { get; set; } = null!;

    /// <summary>
    /// Initializes the ScriptActor with its compiled script, trigger configuration, and supporting services.
    /// </summary>
    /// <param name="scriptName">Name of the script this actor manages.</param>
    /// <param name="instanceName">Unique name of the owning instance.</param>
    /// <param name="instanceActor">Reference to the parent Instance Actor.</param>
    /// <param name="compiledScript">Pre-compiled Roslyn script delegate, or null when compilation failed.</param>
    /// <param name="scriptConfig">Resolved script metadata including trigger type and configuration.</param>
    /// <param name="sharedScriptLibrary">Library of compiled shared scripts available for inline execution.</param>
    /// <param name="options">Site runtime configuration options.</param>
    /// <param name="logger">Logger for diagnostics.</param>
    /// <param name="compiledTriggerExpression">Pre-compiled boolean trigger expression, or null when not an expression trigger.</param>
    /// <param name="initialAttributes">Initial attribute snapshot used to seed expression trigger evaluation state.</param>
    /// <param name="healthCollector">Optional health metrics collector.</param>
    /// <param name="serviceProvider">Optional DI service provider for script execution context services.</param>
    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;
        _executionTimeoutSeconds = scriptConfig.ExecutionTimeoutSeconds;
        _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.
        SeedAttributesReference = initialAttributes;
        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 WhileTrue re-fire tick
        Receive<WhileTrueTick>(_ => FireWhileTrueTick());

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

    /// <inheritdoc />
    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);
    }

    /// <inheritdoc />
    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;
        }

        // Audit Log #23 (ParentExecutionId): carry any inbound-routed
        // ParentExecutionId through to the ScriptExecutionActor so the routed
        // script's ScriptRuntimeContext can record its spawner. Null for normal
        // (tag-change / timer) runs and nested Script.Call invocations.
        SpawnExecution(
            request.Parameters, request.CurrentCallDepth, Sender, request.CorrelationId,
            request.ParentExecutionId);
    }

    /// <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)
            {
                var conditionMet = EvaluateCondition(conditional, changed.Value);
                if (conditional.Mode == TriggerMode.WhileTrue)
                {
                    // Edge-detect against the prior truth value; the timer does
                    // the repeated firing while the condition stays true.
                    HandleWhileTrueTransition(conditionMet, _conditionState);
                    _conditionState = conditionMet;
                }
                else if (conditionMet)
                {
                    // OnTrue: fire on each matching change (existing behavior).
                    TrySpawnExecution(null);
                }
            }
        }
        else if (_triggerConfig is ExpressionTriggerConfig)
        {
            EvaluateExpressionTrigger();
        }
    }

    /// <summary>
    /// Evaluates the compiled trigger expression against the current attribute
    /// snapshot. In <see cref="TriggerMode.OnTrue"/> mode the script runs once
    /// per false→true transition; in <see cref="TriggerMode.WhileTrue"/> mode it
    /// fires on the edge and the re-fire timer is started/stopped with the
    /// expression's truth value. 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;
        if (_triggerConfig is not ExpressionTriggerConfig exprConfig) 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 (exprConfig.Mode == TriggerMode.WhileTrue)
        {
            HandleWhileTrueTransition(result, _lastExpressionResult);
        }
        else if (result && !_lastExpressionResult)
        {
            TrySpawnExecution(null);
        }

        _lastExpressionResult = result;
    }

    /// <summary>
    /// Applies a WhileTrue trigger's condition-state transition: on the
    /// false→true edge, fire once and start the re-fire timer; on the
    /// true→false edge, stop the timer. While the state is unchanged, the
    /// already-running timer continues to drive re-firing.
    /// </summary>
    private void HandleWhileTrueTransition(bool nowTrue, bool wasTrue)
    {
        if (nowTrue && !wasTrue)
        {
            TrySpawnExecution(null);
            StartWhileTrueTimer();
        }
        else if (!nowTrue && wasTrue)
        {
            StopWhileTrueTimer();
        }
    }

    /// <summary>
    /// Starts the periodic WhileTrue re-fire timer. The cadence is the script's
    /// <c>MinTimeBetweenRuns</c>; with none configured the trigger cannot
    /// re-fire, so it degrades to the single edge fire and logs a warning.
    /// </summary>
    private void StartWhileTrueTimer()
    {
        if (_compiledScript == null) return;

        if (_minTimeBetweenRuns is not { } interval)
        {
            _logger.LogWarning(
                "ScriptActor {Script} on {Instance}: WhileTrue trigger has no MinTimeBetweenRuns — " +
                "firing once on the edge only, no re-fire timer.",
                _scriptName, _instanceName);
            return;
        }

        Timers.StartPeriodicTimer(WhileTrueTimerKey, WhileTrueTick.Instance, interval, interval);
    }

    /// <summary>Cancels the WhileTrue re-fire timer (a no-op if it is not running).</summary>
    private void StopWhileTrueTimer() => Timers.Cancel(WhileTrueTimerKey);

    /// <summary>
    /// Fires the script for a WhileTrue re-fire tick. The timer interval is
    /// itself the cadence, so this spawns directly — bypassing the
    /// MinTimeBetweenRuns skip-check that gates change-driven spawns (which
    /// could otherwise drop a tick to sub-millisecond timing jitter).
    /// </summary>
    private void FireWhileTrueTick()
    {
        if (_compiledScript == null) return;

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

    /// <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,
        Guid? parentExecutionId = null)
    {
        var executionId = $"{_scriptName}-exec-{_executionCounter++}";

        // SiteRuntime-009: the actor's mailbox stays on the default dispatcher, but the
        // script body itself runs on the dedicated ScriptExecutionScheduler (a bounded
        // set of dedicated threads), so blocking script I/O is contained there and
        // cannot starve the shared .NET thread pool.
        var props = Props.Create(() => new ScriptExecutionActor(
            _scriptName,
            _instanceName,
            _compiledScript!,
            parameters,
            callDepth,
            _instanceActor,
            _sharedScriptLibrary,
            _options,
            replyTo,
            correlationId,
            _logger,
            _scope,
            _healthCollector,
            _serviceProvider,
            // Audit Log #23 (ParentExecutionId): null for trigger-driven runs;
            // an inbound-API-routed call supplies the inbound request's id.
            parentExecutionId,
            // M2.5 (#9): per-script timeout override (null = use global).
            _executionTimeoutSeconds));

        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
        {
            // Use InvariantCulture so a string attribute value like "1.5" parses
            // consistently regardless of the host locale (SiteRuntime-023). For
            // purely-numeric inputs the culture argument is a no-op, but it is
            // safe and future-proof for string-typed attribute values arriving
            // from scripts or the data connection layer.
            var numericValue = Convert.ToDouble(value, CultureInfo.InvariantCulture);
            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);
        if (expr == null) return null;

        // ExtractExpression already proved the JSON parses; read the mode too.
        var mode = TriggerMode.OnTrue;
        try
        {
            using var doc = JsonDocument.Parse(json!);
            mode = ParseTriggerMode(doc.RootElement);
        }
        catch (JsonException) { /* keep OnTrue */ }

        return new ExpressionTriggerConfig(expr, mode);
    }

    /// <summary>
    /// Reads the optional <c>mode</c> field (Conditional + Expression triggers).
    /// An absent or unrecognized value (case-insensitive) yields
    /// <see cref="TriggerMode.OnTrue"/>, so pre-WhileTrue configs are unchanged.
    /// </summary>
    private static TriggerMode ParseTriggerMode(JsonElement root)
    {
        var raw = root.TryGetProperty("mode", out var m) ? m.GetString() : null;
        return string.Equals(raw?.Trim(), "WhileTrue", StringComparison.OrdinalIgnoreCase)
            ? TriggerMode.WhileTrue
            : TriggerMode.OnTrue;
    }

    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, ParseTriggerMode(doc.RootElement));
        }
        catch { return null; }
    }

    // ── Internal messages ──

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

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

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

// ── Trigger config types ──

/// <summary>
/// When a Conditional/Expression trigger fires. <see cref="OnTrue"/> fires once
/// as the condition becomes true; <see cref="WhileTrue"/> additionally re-fires
/// on a timer (cadence = the script's MinTimeBetweenRuns) until it goes false.
/// </summary>
internal enum TriggerMode { OnTrue, WhileTrue }

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