using ScadaLink.Commons.Types;
using ScadaLink.Commons.Types.Scripts;

namespace ScadaLink.CentralUI.ScriptAnalysis;

/// <summary>
/// Runtime globals for the Test Run sandbox. Mirrors the real site-runtime
/// <c>ScriptGlobals</c> surface (ScadaLink.SiteRuntime.Scripts) member-for-member
/// so the same user code that runs at a site also compiles and runs here.
///
/// Instance-context members — <c>Instance.GetAttribute/SetAttribute/CallScript</c>,
/// <c>Attributes</c>, <c>Children</c>, <c>Parent</c> — need a live deployed
/// instance. With no instance bound they throw <see cref="ScriptSandboxException"/>;
/// with one bound (see <see cref="SandboxInstanceContext"/>) they route to it.
///
/// <c>ExternalSystem</c>, <c>Database</c>, <c>Notify</c>, and
/// <c>Scripts.CallShared</c> run against central's real services and fire for
/// real — they do not depend on a bound instance.
/// </summary>
public class SandboxScriptHost
{
    public ScriptParameters Parameters { get; init; } = new();

    public CancellationToken CancellationToken { get; init; }

    public AlarmContext? Alarm { get; init; }

    public ScriptScope Scope { get; init; } = ScriptScope.Root;

    public SandboxInstanceContext Instance { get; init; } = new();

    public SandboxExternalHelper ExternalSystem => Instance.ExternalSystem;
    public SandboxDatabaseHelper Database => Instance.Database;
    public SandboxNotifyHelper Notify => Instance.Notify;
    public SandboxScriptCallHelper Scripts => Instance.Scripts;

    public SandboxAttributeAccessor Attributes => new(Instance, Scope.SelfPath);
    public SandboxChildrenAccessor Children => new(Instance, Scope.SelfPath);
    public SandboxCompositionAccessor? Parent =>
        Scope.ParentPath == null ? null : new SandboxCompositionAccessor(Instance, Scope.ParentPath);
}

/// <summary>
/// Backs the sandbox <c>Instance</c> when a Test Run is bound to a real
/// deployed instance. Null when unbound. The implementation routes to the
/// instance cross-site over the cluster transport.
/// </summary>
public interface ISandboxInstanceGateway
{
    Task<object?> GetAttributeAsync(string canonicalName, CancellationToken ct);
    Task SetAttributeAsync(string canonicalName, string value, CancellationToken ct);
    Task<object?> CallScriptAsync(
        string canonicalScriptName, IReadOnlyDictionary<string, object?>? parameters, CancellationToken ct);
}

/// <summary>
/// Sandbox mirror of <c>ScadaLink.SiteRuntime.Scripts.ScriptRuntimeContext</c> —
/// the <c>Instance</c> global. Attribute and sibling-script access needs a real
/// deployed instance: with no gateway wired it throws; with one (a bound
/// instance) it routes cross-site. <c>ExternalSystem</c>/<c>Database</c>/
/// <c>Notify</c>/<c>Scripts</c> run against central's real services regardless
/// of binding.
/// </summary>
public class SandboxInstanceContext
{
    private readonly ISandboxInstanceGateway? _gateway;

    public SandboxExternalHelper ExternalSystem { get; }
    public SandboxDatabaseHelper Database { get; }
    public SandboxNotifyHelper Notify { get; }
    public SandboxScriptCallHelper Scripts { get; }

    public SandboxInstanceContext(
        ISandboxInstanceGateway? gateway = null,
        SandboxExternalHelper? external = null,
        SandboxDatabaseHelper? database = null,
        SandboxNotifyHelper? notify = null,
        SandboxScriptCallHelper? scripts = null)
    {
        _gateway = gateway;
        ExternalSystem = external ?? new SandboxExternalHelper(null, "<sandbox>");
        Database = database ?? new SandboxDatabaseHelper(null, "<sandbox>");
        Notify = notify ?? new SandboxNotifyHelper(null, "<sandbox>");
        Scripts = scripts ?? new SandboxScriptCallHelper(null);
    }

    public Task<object?> GetAttribute(string attributeName)
    {
        if (_gateway == null)
            throw new ScriptSandboxException(
                $"GetAttribute(\"{attributeName}\") needs a deployed instance — " +
                "bind one in Test Run to read live attribute values.");
        return _gateway.GetAttributeAsync(attributeName, CancellationToken.None);
    }

    public void SetAttribute(string attributeName, string value)
    {
        if (_gateway == null)
            throw new ScriptSandboxException(
                $"SetAttribute(\"{attributeName}\") needs a deployed instance — " +
                "bind one in Test Run to write attribute values.");
        _gateway.SetAttributeAsync(attributeName, value, CancellationToken.None).GetAwaiter().GetResult();
    }

    public Task<object?> CallScript(string scriptName, object? parameters = null)
    {
        if (_gateway == null)
            throw new ScriptSandboxException(
                $"CallScript(\"{scriptName}\") needs a deployed instance — " +
                "bind one in Test Run to call sibling scripts.");
        return _gateway.CallScriptAsync(scriptName, ScriptArgs.Normalize(parameters), CancellationToken.None);
    }
}

/// <summary>
/// Sandbox mirror of <c>ScriptRuntimeContext.ScriptCallHelper</c> —
/// <c>Scripts.CallShared(...)</c>. Compiles and runs the named shared script in
/// the same sandbox via the wired delegate.
/// </summary>
public class SandboxScriptCallHelper
{
    private readonly Func<string, IReadOnlyDictionary<string, object?>?, CancellationToken, Task<object?>>? _callShared;

    public SandboxScriptCallHelper(
        Func<string, IReadOnlyDictionary<string, object?>?, CancellationToken, Task<object?>>? callShared)
    {
        _callShared = callShared;
    }

    public Task<object?> CallShared(
        string scriptName,
        object? parameters = null,
        CancellationToken cancellationToken = default)
    {
        if (_callShared == null)
            throw new ScriptSandboxException(
                $"Scripts.CallShared(\"{scriptName}\") — shared-script catalog not configured for Test Run.");
        return _callShared(scriptName, ScriptArgs.Normalize(parameters), cancellationToken);
    }
}

/// <summary>
/// Sandbox mirror of <c>ScadaLink.SiteRuntime.Scripts.AttributeAccessor</c> —
/// scope-aware <c>Attributes["X"]</c> access anchored at a canonical-name prefix.
/// </summary>
public class SandboxAttributeAccessor
{
    private readonly SandboxInstanceContext _ctx;

    public string ScopePrefix { get; }

    public SandboxAttributeAccessor(SandboxInstanceContext ctx, string prefix)
    {
        _ctx = ctx;
        ScopePrefix = prefix;
    }

    public string Resolve(string key) =>
        ScopePrefix.Length == 0 ? key : ScopePrefix + "." + key;

    public object? this[string key]
    {
        get => _ctx.GetAttribute(Resolve(key)).GetAwaiter().GetResult();
        set => _ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty);
    }

    public Task<object?> GetAsync(string key) => _ctx.GetAttribute(Resolve(key));

    public Task SetAsync(string key, object? value)
    {
        _ctx.SetAttribute(Resolve(key), value?.ToString() ?? string.Empty);
        return Task.CompletedTask;
    }
}

/// <summary>
/// Sandbox mirror of <c>ScadaLink.SiteRuntime.Scripts.CompositionAccessor</c> —
/// a view of one composition: its attributes plus an invokable <c>CallScript</c>.
/// </summary>
public class SandboxCompositionAccessor
{
    private readonly SandboxInstanceContext _ctx;

    public string Path { get; }

    public SandboxAttributeAccessor Attributes { get; }

    public SandboxCompositionAccessor(SandboxInstanceContext ctx, string path)
    {
        _ctx = ctx;
        Path = path;
        Attributes = new SandboxAttributeAccessor(ctx, path);
    }

    public string ResolveScript(string scriptName) =>
        Path.Length == 0 ? scriptName : Path + "." + scriptName;

    public Task<object?> CallScript(string scriptName, object? parameters = null)
        => _ctx.CallScript(ResolveScript(scriptName), parameters);
}

/// <summary>
/// Sandbox mirror of <c>ScadaLink.SiteRuntime.Scripts.ChildrenAccessor</c> —
/// dictionary-style access to child compositions.
/// </summary>
public class SandboxChildrenAccessor
{
    private readonly SandboxInstanceContext _ctx;
    private readonly string _selfPath;

    public SandboxChildrenAccessor(SandboxInstanceContext ctx, string selfPath)
    {
        _ctx = ctx;
        _selfPath = selfPath;
    }

    public SandboxCompositionAccessor this[string compositionName]
    {
        get
        {
            var path = _selfPath.Length == 0
                ? compositionName
                : _selfPath + "." + compositionName;
            return new SandboxCompositionAccessor(_ctx, path);
        }
    }
}

/// <summary>
/// Distinct exception so the Test Run pipeline can label sandbox-only
/// limitations differently from genuine runtime errors in user code.
/// </summary>
public class ScriptSandboxException : Exception
{
    public ScriptSandboxException(string message) : base(message) { }
}
