@namespace ZB.MOM.WW.ScadaBridge.CentralUI.Components.Shared
@implements IAsyncDisposable
@inject IJSRuntime JS
@inject Microsoft.Extensions.Logging.ILogger<MonacoEditor> Logger

@if (ShowToolbar)
{
    <div class="d-flex justify-content-end align-items-center gap-3 mb-1 small text-muted">
        <button type="button" class="btn btn-link btn-sm p-0 text-decoration-none" @onclick="FormatAsync"
                title="Format document (Ctrl/Cmd+Shift+F)">Format</button>
        <button type="button" class="btn btn-link btn-sm p-0 text-decoration-none" @onclick="ToggleWrap"
                title="Word wrap">@(_wrap ? "Wrap on" : "Wrap off")</button>
        <button type="button" class="btn btn-link btn-sm p-0 text-decoration-none" @onclick="ToggleMinimap"
                title="Toggle minimap">@(_minimap ? "Minimap on" : "Minimap off")</button>
        <button type="button" class="btn btn-link btn-sm p-0 text-decoration-none" @onclick="ToggleTheme"
                title="Toggle theme">@(_dark ? "Dark" : "Light")</button>
    </div>
}

<div @ref="_hostRef" class="monaco-editor-host"
     style="height: @Height; border: 1px solid var(--bs-border-color); border-radius: 0.25rem; overflow: hidden;"></div>

@code {
    [Parameter] public string Value { get; set; } = "";
    [Parameter] public EventCallback<string> ValueChanged { get; set; }
    [Parameter] public string Language { get; set; } = "csharp";
    [Parameter] public string Height { get; set; } = "320px";
    [Parameter] public bool ReadOnly { get; set; } = false;
    [Parameter] public bool ShowToolbar { get; set; } = true;

    /// <summary>
    /// Runtime globals surface the script is analyzed against. Defaults to
    /// template/shared-script globals; set to <c>InboundApi</c> on the API
    /// method editor so <c>Route</c> and <c>Parameters</c> type-check.
    /// </summary>
    [Parameter] public ScriptAnalysis.ScriptKind ScriptKind { get; set; } = ScriptAnalysis.ScriptKind.Template;

    /// <summary>
    /// Parameter names declared on the form (derived from the SchemaBuilder's
    /// JSON Schema), surfaced as completions inside Parameters["..."] literals
    /// and used by the unknown-key diagnostic.
    /// </summary>
    [Parameter] public IReadOnlyList<string>? DeclaredParameters { get; set; }

    /// <summary>
    /// Full shapes (name + type + required) for the declared parameters.
    /// Used by Parameters["name"] hover to show the declared type. If null,
    /// derived from <see cref="DeclaredParameters"/> with type "Object".
    /// </summary>
    [Parameter] public IReadOnlyList<ScriptAnalysis.ParameterShape>? DeclaredParameterShapes { get; set; }

    /// <summary>
    /// Shapes (name + parameter list + return type) of other scripts on the
    /// same template. Surfaced inside CallScript("...") for completion,
    /// signature help, hover, and argument-count diagnostics.
    /// </summary>
    [Parameter] public IReadOnlyList<ScriptAnalysis.ScriptShape>? SiblingScripts { get; set; }

    /// <summary>
    /// Attributes declared on the current template. Surfaced inside
    /// <c>Attributes["..."]</c> for completion and SCADA006 diagnostics.
    /// </summary>
    [Parameter] public IReadOnlyList<ScriptAnalysis.AttributeShape>? SelfAttributes { get; set; }

    /// <summary>
    /// Child compositions on the current template, each with its template's
    /// attributes and scripts. Surfaced for <c>Children["X"].Attributes</c>,
    /// <c>Children["X"].CallScript</c>, and SCADA007 diagnostics.
    /// </summary>
    [Parameter] public IReadOnlyList<ScriptAnalysis.CompositionContext>? Children { get; set; }

    /// <summary>
    /// Parent template when the current template is composed inside exactly
    /// one other template. <c>null</c> at the root or when multiple parents
    /// exist. Surfaced for <c>Parent.Attributes</c> / <c>Parent.CallScript</c>.
    /// </summary>
    [Parameter] public ScriptAnalysis.CompositionContext? Parent { get; set; }

    /// <summary>
    /// Fires whenever Monaco's marker set updates (after the 500 ms diagnostic
    /// debounce). Hosts can render a <see cref="ProblemsPanel"/> with the same
    /// data.
    /// </summary>
    [Parameter] public EventCallback<IReadOnlyList<ScriptAnalysis.DiagnosticMarker>> MarkersChanged { get; set; }

    private ElementReference _hostRef;
    private DotNetObjectReference<MonacoEditor>? _dotNetRef;
    private readonly string _id = Guid.NewGuid().ToString("N");
    private string _lastSentValue = "";
    private bool _initialized;
    private bool _wrap;
    private bool _minimap;
    private bool _dark;

    protected override async Task OnAfterRenderAsync(bool firstRender)
    {
        if (firstRender)
        {
            _dotNetRef = DotNetObjectReference.Create(this);
            _lastSentValue = Value ?? "";
            try
            {
                await JS.InvokeVoidAsync(
                    "MonacoBlazor.createEditor",
                    _id,
                    _hostRef,
                    new
                    {
                        value = Value ?? "",
                        language = Language,
                        readOnly = ReadOnly
                    },
                    _dotNetRef);
                _initialized = true;
            }
            catch (InvalidOperationException)
            {
                // Prerendering: JS interop is not available yet — the next
                // (interactive) render retries. Expected, not logged.
            }
            catch (JSDisconnectedException)
            {
                // Circuit disconnected before init completed — nothing to do.
            }
            catch (JSException ex)
            {
                // A genuine Monaco init failure — surface it instead of hiding it.
                Logger.LogError(ex, "Monaco editor {EditorId} failed to initialize.", _id);
            }
        }
        else if (_initialized && (Value ?? "") != _lastSentValue)
        {
            _lastSentValue = Value ?? "";
            await SafeInvokeAsync("MonacoBlazor.setValue", "set editor value", _id, _lastSentValue);
        }
    }

    /// <summary>
    /// Invokes a Monaco JS function, swallowing the expected disconnect case but
    /// logging any genuine JS error (CentralUI-018) so failures are not silent.
    /// </summary>
    private async ValueTask SafeInvokeAsync(string fn, string action, params object?[] args)
    {
        try
        {
            await JS.InvokeVoidAsync(fn, args);
        }
        catch (JSDisconnectedException)
        {
            // Circuit gone — the editor no longer exists; nothing to log.
        }
        catch (JSException ex)
        {
            Logger.LogWarning(ex, "Monaco editor {EditorId}: failed to {Action}.", _id, action);
        }
    }

    [JSInvokable]
    public Task OnValueChanged(string newValue)
    {
        _lastSentValue = newValue ?? "";
        return ValueChanged.InvokeAsync(_lastSentValue);
    }

    [JSInvokable]
    public Task OnMarkersChanged(ScriptAnalysis.DiagnosticMarker[] markers) =>
        MarkersChanged.InvokeAsync(markers ?? Array.Empty<ScriptAnalysis.DiagnosticMarker>());

    /// <summary>Programmatic scroll-to-line (called by the problems panel).</summary>
    public async Task RevealLineAsync(int line, int column = 1)
    {
        if (!_initialized) return;
        await SafeInvokeAsync("MonacoBlazor.revealLine", "reveal line", _id, line, column);
    }

    /// <summary>
    /// Called from JS at completion-request time so the form's latest state is
    /// passed through, not whatever was captured when the editor was created.
    /// </summary>
    [JSInvokable]
    public ScadaContext GetContext() => new(
        DeclaredParameters?.ToArray() ?? Array.Empty<string>(),
        SiblingScripts?.ToArray() ?? Array.Empty<ScriptAnalysis.ScriptShape>(),
        DeclaredParameterShapes?.ToArray()
            ?? DeclaredParameters?.Select(n => new ScriptAnalysis.ParameterShape(n, "Object", true)).ToArray()
            ?? Array.Empty<ScriptAnalysis.ParameterShape>(),
        SelfAttributes?.ToArray() ?? Array.Empty<ScriptAnalysis.AttributeShape>(),
        Children?.ToArray() ?? Array.Empty<ScriptAnalysis.CompositionContext>(),
        Parent,
        ScriptKind);

    private async Task FormatAsync()
    {
        if (!_initialized) return;
        await SafeInvokeAsync("MonacoBlazor.format", "format document", _id);
    }

    private async Task ToggleWrap()
    {
        _wrap = !_wrap;
        await SafeInvokeAsync("MonacoBlazor.setEditorOption", "toggle word wrap", _id, "wordWrap", _wrap ? "on" : "off");
    }

    private async Task ToggleMinimap()
    {
        _minimap = !_minimap;
        await SafeInvokeAsync("MonacoBlazor.setEditorOption", "toggle minimap", _id, "minimap", new { enabled = _minimap });
    }

    private async Task ToggleTheme()
    {
        _dark = !_dark;
        await SafeInvokeAsync("MonacoBlazor.setEditorOption", "toggle theme", _id, "theme", _dark ? "vs-dark" : "vs");
    }

    public async ValueTask DisposeAsync()
    {
        if (_initialized)
        {
            // Disposal commonly races a circuit disconnect — JSDisconnectedException
            // here is expected and silent; a real JSException is still logged.
            await SafeInvokeAsync("MonacoBlazor.dispose", "dispose editor", _id);
        }
        _dotNetRef?.Dispose();
    }

    public record ScadaContext(
        string[] DeclaredParameters,
        ScriptAnalysis.ScriptShape[] SiblingScripts,
        ScriptAnalysis.ParameterShape[] DeclaredParameterShapes,
        ScriptAnalysis.AttributeShape[] SelfAttributes,
        ScriptAnalysis.CompositionContext[] Children,
        ScriptAnalysis.CompositionContext? Parent,
        ScriptAnalysis.ScriptKind ScriptKind);
}
