@page "/design/api-methods/create"
@page "/design/api-methods/{Id:int}/edit"
@using ZB.MOM.WW.ScadaBridge.Security
@using ZB.MOM.WW.ScadaBridge.Commons.Entities.InboundApi
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Repositories
@using ZB.MOM.WW.ScadaBridge.Commons.Interfaces.Security
@using ZB.MOM.WW.ScadaBridge.CentralUI.Services
@using ScriptAnalysis = ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis
@attribute [Authorize(Policy = AuthorizationPolicies.RequireDesign)]
@inject IInboundApiRepository InboundApiRepository
@inject IInboundApiKeyAdmin ApiKeyAdmin
@inject ScriptAnalysis.ScriptAnalysisService AnalysisService
@inject NavigationManager NavigationManager

<div class="container-fluid mt-3">
    <button class="btn btn-link text-decoration-none ps-0 mb-2" @onclick="GoBack">&larr; Back</button>

    <h4 class="mb-3">@(Id.HasValue ? "Edit API Method" : "Add API Method")</h4>

    @if (_loading)
    {
        <LoadingSpinner IsLoading="true" />
    }
    else
    {
        <div class="card">
            <div class="card-body">
                <div class="mb-3">
                    <label class="form-label">Name</label>
                    <input type="text" class="form-control" @bind="_name" disabled="@Id.HasValue" />
                </div>
                <div class="mb-3">
                    <label class="form-label">Timeout (seconds)</label>
                    <input type="number" class="form-control" @bind="_timeoutSeconds" min="1" />
                </div>
                <div class="mb-3">
                    <label class="form-label">Approved API Keys</label>
                    @if (_allKeys.Count == 0)
                    {
                        <div class="form-text">
                            No API keys configured.
                            <a href="/admin/api-keys">Create one</a> to authorize callers for this method.
                        </div>
                    }
                    else
                    {
                        <div class="border rounded p-2" style="max-height: 220px; overflow-y: auto;">
                            @foreach (var key in _allKeys)
                            {
                                var checkboxId = $"approved-key-{key.KeyId}";
                                <div class="form-check">
                                    <input class="form-check-input" type="checkbox" id="@checkboxId"
                                           checked="@_selectedKeyIds.Contains(key.KeyId)"
                                           @onchange="e => ToggleKey(key.KeyId, (bool)e.Value!)" />
                                    <label class="form-check-label" for="@checkboxId">
                                        @key.Name
                                        @if (!key.Enabled)
                                        {
                                            <span class="badge bg-secondary ms-1">Disabled</span>
                                        }
                                    </label>
                                </div>
                            }
                        </div>
                        <div class="form-text">
                            Callers must present a checked key in the <code>X-API-Key</code> header to invoke this method.
                        </div>
                    }
                </div>
                <div class="mb-3">
                    <label class="form-label">Parameters</label>
                    <SchemaBuilder Mode="object"
                                   Value="@_params"
                                   ValueChanged="@(v => _params = v)" />
                </div>
                <div class="mb-3">
                    <label class="form-label">Return value</label>
                    <SchemaBuilder Mode="value"
                                   Value="@_returns"
                                   ValueChanged="@(v => _returns = v)" />
                </div>
                <div class="mb-3">
                    <label class="form-label">Script</label>
                    <MonacoEditor @ref="_editor" Value="@_script" ValueChanged="@(v => _script = v)"
                                  Language="csharp" Height="320px"
                                  ScriptKind="ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.ScriptKind.InboundApi"
                                  DeclaredParameters="@ScriptParameterNames.Parse(_params)"
                                  DeclaredParameterShapes="@ScriptParameterNames.ParseShapes(_params)"
                                  MarkersChanged="@(m => { _markers = m; StateHasChanged(); })" />
                    <ProblemsPanel Markers="@_markers" OnNavigate="@(m => _editor?.RevealLineAsync(m.StartLineNumber, m.StartColumn) ?? Task.CompletedTask)" />
                </div>

                @if (_formError != null)
                {
                    <div class="text-danger small mb-2">@_formError</div>
                }

                <div class="d-flex gap-2">
                    <button class="btn btn-success" @onclick="Save">Save</button>
                    <button class="btn btn-outline-primary" @onclick="ToggleTestRunPanel">
                        @(_showTestRun ? "Hide Test Run" : "Test Run")
                    </button>
                    <button class="btn btn-outline-secondary" @onclick="GoBack">Cancel</button>
                </div>
            </div>
        </div>

        @if (_showTestRun)
        {
            <div class="card mt-3" id="test-run-panel">
                <div class="card-header py-2">
                    <span class="fw-semibold">Test Run</span>
                </div>
                <div class="alert alert-warning py-1 mb-0 small rounded-0 border-0 border-bottom">
                    <strong>Heads up:</strong>
                    runs the script as typed (unsaved edits included) against the supplied
                    <code>Parameters</code>. <code>Route</code> calls throw — cross-site
                    routing needs a deployed site reachable over the cluster transport.
                </div>
                <div class="card-body">
                    <div class="mb-3">
                        <label class="form-label small">Parameter values</label>
                        <ParameterValueForm ParameterDefinitions="@_params"
                                            Values="_paramValues"
                                            ValuesChanged="@(v => _paramValues = v)" />
                    </div>
                    <div class="d-flex gap-2 align-items-center mb-3">
                        <button class="btn btn-primary btn-sm" @onclick="RunInSandboxAsync" disabled="@_running">
                            @if (_running)
                            {
                                <span class="spinner-border spinner-border-sm me-1" role="status" aria-hidden="true"></span>
                                <span>Running…</span>
                            }
                            else
                            {
                                <span>Run</span>
                            }
                        </button>
                        @if (_runResult != null)
                        {
                            <span class="text-muted small">@_runResult.DurationMs ms</span>
                        }
                    </div>

                    @if (_runResult != null)
                    {
                        @if (_runResult.Success)
                        {
                            <div class="mb-3">
                                <label class="form-label small text-success mb-1">
                                    Return value <span class="badge bg-light text-dark ms-1">@_runResult.ReturnTypeName</span>
                                </label>
                                <pre class="bg-light border rounded p-2 small mb-0 font-monospace" style="white-space: pre-wrap;">@_runResult.ReturnValueJson</pre>
                            </div>
                        }
                        else
                        {
                            <div class="mb-3">
                                <label class="form-label small text-danger mb-1">
                                    <span class="badge bg-danger me-1">@ErrorKindLabel(_runResult.ErrorKind)</span>
                                </label>
                                <pre class="border border-danger-subtle rounded p-2 small mb-0 font-monospace text-danger" style="white-space: pre-wrap;">@_runResult.Error</pre>
                                @if (_runResult.Markers is { Count: > 0 })
                                {
                                    <ul class="small text-danger mt-2 mb-0">
                                        @foreach (var m in _runResult.Markers)
                                        {
                                            <li>Line @m.StartLineNumber, col @m.StartColumn: @m.Message <code class="ms-1">@m.Code</code></li>
                                        }
                                    </ul>
                                }
                            </div>
                        }

                        @if (!string.IsNullOrEmpty(_runResult.ConsoleOutput))
                        {
                            <div class="mb-0">
                                <label class="form-label small mb-1">Console output</label>
                                <pre class="bg-dark text-light rounded p-2 small mb-0 font-monospace" style="white-space: pre-wrap;">@_runResult.ConsoleOutput</pre>
                            </div>
                        }
                    }
                </div>
            </div>
        }
    }
</div>

@code {
    [Parameter] public int? Id { get; set; }

    private bool _loading = true;
    private string _name = "", _script = "";
    private int _timeoutSeconds = 30;
    private string? _params, _returns;
    private string? _formError;
    private MonacoEditor? _editor;
    private IReadOnlyList<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.DiagnosticMarker> _markers
        = Array.Empty<ZB.MOM.WW.ScadaBridge.CentralUI.ScriptAnalysis.DiagnosticMarker>();

    // Inbound-API key re-arch (C3): the approved-keys list is driven by the IInboundApiKeyAdmin
    // seam, not ApiMethod.ApprovedApiKeyIds. The ApiMethod entity itself (name/script/params/etc.)
    // still lives on IInboundApiRepository — only the key↔method approval relationship moved to
    // per-key method-scopes. Keys are identified by an opaque string KeyId.
    private ApiMethod? _existing;
    private List<InboundApiKeyInfo> _allKeys = new();
    private HashSet<string> _selectedKeyIds = new(StringComparer.Ordinal);
    // Keys approved for this method when the form loaded (empty on create), for diffing on save.
    private HashSet<string> _initialKeyIds = new(StringComparer.Ordinal);

    private bool _showTestRun;
    private bool _running;
    private Dictionary<string, object?> _paramValues = new();
    private ScriptAnalysis.SandboxRunResult? _runResult;
    private CancellationTokenSource? _runCts;

    protected override async Task OnInitializedAsync()
    {
        try
        {
            // All keys come from the seam (hash-free projection).
            _allKeys = (await ApiKeyAdmin.ListAsync()).ToList();
        }
        catch (Exception ex) { _formError = ex.Message; }

        if (Id.HasValue)
        {
            try
            {
                _existing = await InboundApiRepository.GetApiMethodByIdAsync(Id.Value);
                if (_existing != null)
                {
                    _name = _existing.Name;
                    _script = _existing.Script;
                    _timeoutSeconds = _existing.TimeoutSeconds;
                    _params = _existing.ParameterDefinitions;
                    _returns = _existing.ReturnDefinition;
                    // Seed approved keys from the seam: which keys' scopes contain this method.
                    var keysForMethod = await ApiKeyAdmin.GetKeysForMethodAsync(_existing.Name);
                    _initialKeyIds = new HashSet<string>(keysForMethod, StringComparer.Ordinal);
                    _selectedKeyIds = new HashSet<string>(_initialKeyIds, StringComparer.Ordinal);
                }
            }
            catch (Exception ex) { _formError = ex.Message; }
        }
        _loading = false;
    }

    private void ToggleKey(string keyId, bool isChecked)
    {
        if (isChecked) _selectedKeyIds.Add(keyId);
        else _selectedKeyIds.Remove(keyId);
    }

    private async Task Save()
    {
        _formError = null;
        if (string.IsNullOrWhiteSpace(_name) || string.IsNullOrWhiteSpace(_script))
        {
            _formError = "Name and script required.";
            return;
        }

        try
        {
            // Save the ApiMethod entity FIRST so the method Name exists before we reconcile
            // key scopes against it. The method entity stays in SQL Server; we leave the
            // (now-legacy) ApprovedApiKeyIds column untouched — it is dropped in C5.
            string methodName;
            if (_existing != null)
            {
                _existing.Script = _script;
                _existing.TimeoutSeconds = _timeoutSeconds;
                _existing.ParameterDefinitions = _params?.Trim();
                _existing.ReturnDefinition = _returns?.Trim();
                await InboundApiRepository.UpdateApiMethodAsync(_existing);
                methodName = _existing.Name;
            }
            else
            {
                var m = new ApiMethod(_name.Trim(), _script)
                {
                    TimeoutSeconds = _timeoutSeconds,
                    ParameterDefinitions = _params?.Trim(),
                    ReturnDefinition = _returns?.Trim()
                };
                await InboundApiRepository.AddApiMethodAsync(m);
                methodName = m.Name;
            }
            await InboundApiRepository.SaveChangesAsync();

            // Reconcile per-key method-scopes for the affected keys (added/removed vs. load time).
            if (!await ReconcileKeyScopesAsync(methodName)) return;

            NavigationManager.NavigateTo("/design/external-systems");
        }
        catch (Exception ex) { _formError = ex.Message; }
    }

    /// <summary>
    /// Pushes this method's name into / out of each affected key's scope set. Returns false
    /// (leaving a form error) when a revoke would empty a key's scopes — the server rejects
    /// empty scope sets, so we abort rather than push one. Scopes are read fresh per affected
    /// key so we never clobber unrelated method-scopes.
    /// </summary>
    private async Task<bool> ReconcileKeyScopesAsync(string methodName)
    {
        var affected = _selectedKeyIds.Except(_initialKeyIds, StringComparer.Ordinal)
            .Concat(_initialKeyIds.Except(_selectedKeyIds, StringComparer.Ordinal))
            .ToHashSet(StringComparer.Ordinal);

        // Read each affected key's CURRENT full scope set so add/remove preserves other methods.
        var currentMethodsByKey = new Dictionary<string, IReadOnlyList<string>>(StringComparer.Ordinal);
        foreach (var keyId in affected)
        {
            currentMethodsByKey[keyId] = await ApiKeyAdmin.GetMethodsForKeyAsync(keyId);
        }

        var keyNamesById = _allKeys.ToDictionary(k => k.KeyId, k => k.Name, StringComparer.Ordinal);

        var plan = ApiMethodKeyScopeReconciler.Reconcile(
            methodName, _selectedKeyIds, _initialKeyIds, currentMethodsByKey, keyNamesById);

        // Empty-last-scope guard: a key cannot end up with zero scopes (server rejects it).
        if (plan.EmptyScopeKeyNames.Count > 0)
        {
            var names = string.Join(", ", plan.EmptyScopeKeyNames);
            _formError =
                $"Cannot revoke this method from key(s) [{names}] — it would leave them with no methods. " +
                "Disable or delete the key instead, or grant it another method first.";
            return false;
        }

        try
        {
            foreach (var update in plan.Updates)
            {
                var ok = await ApiKeyAdmin.SetMethodsAsync(update.KeyId, update.NewMethods);
                if (!ok)
                    throw new InvalidOperationException(
                        $"Key '{NameFor(update.KeyId)}' was not found in the key store.");
            }
        }
        catch (Exception ex)
        {
            throw new InvalidOperationException(
                $"Method '{methodName}' was saved, but updating approved-key scopes failed partway: {ex.Message} " +
                "Some keys may be partially updated — review them on the API Keys page and retry.", ex);
        }

        // Selection is now the baseline (matters if save is retried without reload).
        _initialKeyIds = new HashSet<string>(_selectedKeyIds, StringComparer.Ordinal);
        return true;
    }

    // Returns the display name for a keyId if available from the loaded key list, else the id itself.
    private string NameFor(string keyId) =>
        _allKeys.FirstOrDefault(k => string.Equals(k.KeyId, keyId, StringComparison.Ordinal))?.Name ?? keyId;

    private void GoBack() => NavigationManager.NavigateTo("/design/external-systems");

    private void ToggleTestRunPanel() => _showTestRun = !_showTestRun;

    private async Task RunInSandboxAsync()
    {
        _runCts?.Cancel();
        _runCts = new CancellationTokenSource();
        _running = true;
        _runResult = null;
        StateHasChanged();

        try
        {
            var jsonParams = _paramValues.ToDictionary(
                kv => kv.Key,
                kv => System.Text.Json.JsonSerializer.SerializeToElement(kv.Value));
            var request = new ScriptAnalysis.SandboxRunRequest(
                _script, jsonParams, TimeoutSeconds: _timeoutSeconds,
                Kind: ScriptAnalysis.ScriptKind.InboundApi);
            _runResult = await AnalysisService.RunInSandboxAsync(request, _runCts.Token);
        }
        catch (OperationCanceledException) { /* superseded by next Run click */ }
        catch (Exception ex)
        {
            _runResult = new ScriptAnalysis.SandboxRunResult(
                Success: false,
                ReturnValueJson: null,
                ReturnTypeName: null,
                ConsoleOutput: "",
                Error: $"Unexpected: {ex.GetType().Name}: {ex.Message}",
                ErrorKind: ScriptAnalysis.SandboxErrorKind.RuntimeError,
                DurationMs: 0,
                Markers: null);
        }
        finally
        {
            _running = false;
            StateHasChanged();
        }
    }

    private static string ErrorKindLabel(ScriptAnalysis.SandboxErrorKind kind) => kind switch
    {
        ScriptAnalysis.SandboxErrorKind.CompileError => "Compile error",
        ScriptAnalysis.SandboxErrorKind.SandboxLimitation => "Sandbox limitation",
        ScriptAnalysis.SandboxErrorKind.RuntimeError => "Runtime error",
        ScriptAnalysis.SandboxErrorKind.Timeout => "Timeout",
        _ => "Error"
    };
}
