using Akka.Actor;
using Microsoft.Extensions.Logging;
using ScadaLink.Commons.Interfaces.Services;
using ScadaLink.Commons.Messages.Instance;
using ScadaLink.Commons.Messages.ScriptExecution;
using ScadaLink.Commons.Types;

namespace ScadaLink.SiteRuntime.Scripts;

/// <summary>
/// WP-18: Script Runtime API — injected into Script/Alarm Execution Actors.
/// Provides the API surface that user scripts interact with:
///   Instance.GetAttribute("name")
///   Instance.SetAttribute("name", value)
///   Instance.CallScript("scriptName", params)
///   Scripts.CallShared("scriptName", params)
///
/// WP-13 (Phase 7): Integration surface APIs:
///   ExternalSystem.Call("systemName", "methodName", params)
///   ExternalSystem.CachedCall("systemName", "methodName", params)
///   Database.Connection("name")
///   Database.CachedWrite("name", "sql", params)
///   Notify.To("listName").Send("subject", "message")
///
/// WP-20: Recursion Limit — call depth tracked and enforced.
/// </summary>
public class ScriptRuntimeContext
{
    private readonly IActorRef _instanceActor;
    private readonly IActorRef _self;
    private readonly SharedScriptLibrary _sharedScriptLibrary;
    private readonly int _currentCallDepth;
    private readonly int _maxCallDepth;
    private readonly TimeSpan _askTimeout;
    private readonly ILogger _logger;
    private readonly string _instanceName;

    /// <summary>
    /// WP-13: External system client for ExternalSystem.Call/CachedCall.
    /// </summary>
    private readonly IExternalSystemClient? _externalSystemClient;

    /// <summary>
    /// WP-13: Database gateway for Database.Connection/CachedWrite.
    /// </summary>
    private readonly IDatabaseGateway? _databaseGateway;

    /// <summary>
    /// WP-13: Notification delivery for Notify.To().Send().
    /// </summary>
    private readonly INotificationDeliveryService? _notificationService;

    public ScriptRuntimeContext(
        IActorRef instanceActor,
        IActorRef self,
        SharedScriptLibrary sharedScriptLibrary,
        int currentCallDepth,
        int maxCallDepth,
        TimeSpan askTimeout,
        string instanceName,
        ILogger logger,
        IExternalSystemClient? externalSystemClient = null,
        IDatabaseGateway? databaseGateway = null,
        INotificationDeliveryService? notificationService = null)
    {
        _instanceActor = instanceActor;
        _self = self;
        _sharedScriptLibrary = sharedScriptLibrary;
        _currentCallDepth = currentCallDepth;
        _maxCallDepth = maxCallDepth;
        _askTimeout = askTimeout;
        _instanceName = instanceName;
        _logger = logger;
        _externalSystemClient = externalSystemClient;
        _databaseGateway = databaseGateway;
        _notificationService = notificationService;
    }

    /// <summary>
    /// Gets the current value of an attribute from the Instance Actor.
    /// Uses Ask pattern (system boundary between script execution and instance state).
    /// </summary>
    public async Task<object?> GetAttribute(string attributeName)
    {
        var correlationId = Guid.NewGuid().ToString();
        var request = new GetAttributeRequest(
            correlationId, _instanceName, attributeName, DateTimeOffset.UtcNow);

        var response = await _instanceActor.Ask<GetAttributeResponse>(request, _askTimeout);

        if (!response.Found)
        {
            _logger.LogWarning(
                "GetAttribute: attribute '{Attribute}' not found on instance '{Instance}'",
                attributeName, _instanceName);
        }

        return response.Value;
    }

    /// <summary>
    /// Sets an attribute value. For data-connected attributes the Instance Actor
    /// forwards the write to the DCL, which writes the physical device; the
    /// in-memory value is not optimistically updated. For static attributes the
    /// Instance Actor updates the in-memory value and persists the override to
    /// SQLite. All mutations are serialized through the Instance Actor mailbox.
    ///
    /// The write is awaited so that a device-write failure on a data-connected
    /// attribute is surfaced synchronously to the calling script as an
    /// <see cref="InvalidOperationException"/>.
    /// </summary>
    public async Task SetAttribute(string attributeName, string value)
    {
        var correlationId = Guid.NewGuid().ToString();
        var command = new SetStaticAttributeCommand(
            correlationId, _instanceName, attributeName, value, DateTimeOffset.UtcNow);

        // Ask — mutation serialized through the Instance Actor mailbox; the reply
        // carries the device-write outcome for data-connected attributes.
        var response = await _instanceActor.Ask<SetStaticAttributeResponse>(command, _askTimeout);

        if (!response.Success)
        {
            throw new InvalidOperationException(
                $"SetAttribute('{attributeName}') failed: {response.ErrorMessage}");
        }
    }

    /// <summary>
    /// Calls a sibling script on the same instance by name (Ask pattern).
    /// WP-20: Enforces recursion limit.
    /// WP-22: Uses Ask pattern for CallScript.
    /// <paramref name="parameters"/> may be a dictionary or an anonymous object
    /// (<c>new { name = "Bob" }</c>) — see <see cref="ScriptArgs"/>.
    /// </summary>
    public async Task<object?> CallScript(string scriptName, object? parameters = null)
    {
        var nextDepth = _currentCallDepth + 1;
        if (nextDepth > _maxCallDepth)
        {
            var msg = $"Script call depth exceeded maximum of {_maxCallDepth}. " +
                      $"CallScript('{scriptName}') rejected at depth {nextDepth}.";
            _logger.LogError(msg);
            throw new InvalidOperationException(msg);
        }

        var correlationId = Guid.NewGuid().ToString();
        var request = new ScriptCallRequest(
            scriptName,
            ScriptArgs.Normalize(parameters),
            nextDepth,
            correlationId);

        // Ask the Instance Actor, which routes to the appropriate Script Actor
        var result = await _instanceActor.Ask<ScriptCallResult>(request, _askTimeout);

        if (!result.Success)
        {
            throw new InvalidOperationException(
                $"CallScript('{scriptName}') failed: {result.ErrorMessage}");
        }

        return result.ReturnValue;
    }

    /// <summary>
    /// Provides access to shared script execution via the Scripts property.
    /// </summary>
    public ScriptCallHelper Scripts => new(_sharedScriptLibrary, this, _currentCallDepth, _maxCallDepth, _logger);

    /// <summary>
    /// WP-13: Provides access to external system calls.
    /// ExternalSystem.Call("systemName", "methodName", params)
    /// ExternalSystem.CachedCall("systemName", "methodName", params)
    /// </summary>
    public ExternalSystemHelper ExternalSystem => new(_externalSystemClient, _instanceName, _logger);

    /// <summary>
    /// WP-13: Provides access to database operations.
    /// Database.Connection("name")
    /// Database.CachedWrite("name", "sql", params)
    /// </summary>
    public DatabaseHelper Database => new(_databaseGateway, _instanceName, _logger);

    /// <summary>
    /// WP-13: Provides access to notification delivery.
    /// Notify.To("listName").Send("subject", "message")
    /// </summary>
    public NotifyHelper Notify => new(_notificationService, _instanceName, _logger);

    /// <summary>
    /// Helper class for Scripts.CallShared() syntax.
    /// </summary>
    public class ScriptCallHelper
    {
        private readonly SharedScriptLibrary _library;
        private readonly ScriptRuntimeContext _context;
        private readonly int _currentCallDepth;
        private readonly int _maxCallDepth;
        private readonly ILogger _logger;

        internal ScriptCallHelper(
            SharedScriptLibrary library,
            ScriptRuntimeContext context,
            int currentCallDepth,
            int maxCallDepth,
            ILogger logger)
        {
            _library = library;
            _context = context;
            _currentCallDepth = currentCallDepth;
            _maxCallDepth = maxCallDepth;
            _logger = logger;
        }

        /// <summary>
        /// WP-17: Executes a shared script inline (direct method call, not actor message).
        /// WP-20: Enforces recursion limit.
        /// <paramref name="parameters"/> may be a dictionary or an anonymous
        /// object (<c>new { name = "Bob" }</c>) — see <see cref="ScriptArgs"/>.
        /// </summary>
        public async Task<object?> CallShared(
            string scriptName,
            object? parameters = null,
            CancellationToken cancellationToken = default)
        {
            var nextDepth = _currentCallDepth + 1;
            if (nextDepth > _maxCallDepth)
            {
                var msg = $"Script call depth exceeded maximum of {_maxCallDepth}. " +
                          $"CallShared('{scriptName}') rejected at depth {nextDepth}.";
                _logger.LogError(msg);
                throw new InvalidOperationException(msg);
            }

            return await _library.ExecuteAsync(
                scriptName, _context, ScriptArgs.Normalize(parameters), cancellationToken);
        }
    }

    /// <summary>
    /// WP-13: Helper for ExternalSystem.Call/CachedCall syntax.
    /// </summary>
    public class ExternalSystemHelper
    {
        private readonly IExternalSystemClient? _client;
        private readonly string _instanceName;
        private readonly ILogger _logger;

        internal ExternalSystemHelper(IExternalSystemClient? client, string instanceName, ILogger logger)
        {
            _client = client;
            _instanceName = instanceName;
            _logger = logger;
        }

        public async Task<ExternalCallResult> Call(
            string systemName,
            string methodName,
            IReadOnlyDictionary<string, object?>? parameters = null,
            CancellationToken cancellationToken = default)
        {
            if (_client == null)
                throw new InvalidOperationException("External system client not available");

            return await _client.CallAsync(systemName, methodName, parameters, cancellationToken);
        }

        public async Task<ExternalCallResult> CachedCall(
            string systemName,
            string methodName,
            IReadOnlyDictionary<string, object?>? parameters = null,
            CancellationToken cancellationToken = default)
        {
            if (_client == null)
                throw new InvalidOperationException("External system client not available");

            return await _client.CachedCallAsync(systemName, methodName, parameters, _instanceName, cancellationToken);
        }
    }

    /// <summary>
    /// WP-13: Helper for Database.Connection/CachedWrite syntax.
    /// </summary>
    public class DatabaseHelper
    {
        private readonly IDatabaseGateway? _gateway;
        private readonly string _instanceName;
        private readonly ILogger _logger;

        internal DatabaseHelper(IDatabaseGateway? gateway, string instanceName, ILogger logger)
        {
            _gateway = gateway;
            _instanceName = instanceName;
            _logger = logger;
        }

        public async Task<System.Data.Common.DbConnection> Connection(
            string name,
            CancellationToken cancellationToken = default)
        {
            if (_gateway == null)
                throw new InvalidOperationException("Database gateway not available");

            return await _gateway.GetConnectionAsync(name, cancellationToken);
        }

        public async Task CachedWrite(
            string name,
            string sql,
            IReadOnlyDictionary<string, object?>? parameters = null,
            CancellationToken cancellationToken = default)
        {
            if (_gateway == null)
                throw new InvalidOperationException("Database gateway not available");

            await _gateway.CachedWriteAsync(name, sql, parameters, _instanceName, cancellationToken);
        }
    }

    /// <summary>
    /// WP-13: Helper for Notify.To("listName").Send("subject", "message") syntax.
    /// </summary>
    public class NotifyHelper
    {
        private readonly INotificationDeliveryService? _service;
        private readonly string _instanceName;
        private readonly ILogger _logger;

        internal NotifyHelper(INotificationDeliveryService? service, string instanceName, ILogger logger)
        {
            _service = service;
            _instanceName = instanceName;
            _logger = logger;
        }

        public NotifyTarget To(string listName)
        {
            return new NotifyTarget(listName, _service, _instanceName, _logger);
        }
    }

    /// <summary>
    /// WP-13: Target for Notify.To("listName").Send("subject", "message").
    /// </summary>
    public class NotifyTarget
    {
        private readonly string _listName;
        private readonly INotificationDeliveryService? _service;
        private readonly string _instanceName;
        private readonly ILogger _logger;

        internal NotifyTarget(string listName, INotificationDeliveryService? service, string instanceName, ILogger logger)
        {
            _listName = listName;
            _service = service;
            _instanceName = instanceName;
            _logger = logger;
        }

        public async Task<NotificationResult> Send(
            string subject,
            string message,
            CancellationToken cancellationToken = default)
        {
            if (_service == null)
                throw new InvalidOperationException("Notification service not available");

            return await _service.SendAsync(_listName, subject, message, _instanceName, cancellationToken);
        }
    }
}
