@namespace ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared
@using Microsoft.Extensions.Logging
@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>
    /// Owning-equipment context for equipment-relative <c>{{equip}}/&lt;RefName&gt;</c> completion +
    /// diagnostics. Set on the per-equipment VirtualTag / ScriptedAlarm modals; left null on the shared
    /// ScriptEdit page (where the token cannot resolve to a single equipment).
    /// </summary>
    [Parameter] public string? EquipmentId { get; set; }

    /// <summary>
    /// Fires whenever Monaco's marker set updates (after the 500 ms diagnostic
    /// debounce). The marker DTO is not modelled yet — typed as object[] until a
    /// later task wires the Roslyn-backed diagnostics.
    /// </summary>
    [Parameter] public EventCallback<object[]> 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,
                        equipmentId = EquipmentId
                    },
                    _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 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)
    {
        var normalized = newValue ?? "";
        if (normalized == _lastSentValue)
            return Task.CompletedTask;
        _lastSentValue = normalized;
        return ValueChanged.InvokeAsync(_lastSentValue);
    }

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

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

    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();
    }
}
