@* Bulk manual tag-entry grid for the v3 /raw tree: add MANY new tags at once under a Device or a
   TagGroup. Each row carries Name / DataType / AccessLevel / WriteIdempotent / PollGroup plus a
   per-row driver-typed TagConfig editor (revealed in an expandable full-width sub-row — the grid stays
   compact). The owning DriverType (fixed by the device) drives the typed-editor dispatch. On commit each
   row is created via CreateTagAsync; committed rows drop off, failed rows stay with their inline error so
   nothing is lost silently. Visibility is @bind-Visible; a successful commit raises OnSaved. *@
@using Microsoft.AspNetCore.Components.Forms
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors
@using ZB.MOM.WW.OtOpcUa.Commons.Types
@using ZB.MOM.WW.OtOpcUa.Configuration.Enums
@inject IRawTreeService Svc

@if (Visible)
{
    <div class="modal-backdrop fade show" style="display:block"></div>
    <div class="modal fade show" tabindex="-1" role="dialog" style="display:block">
        <div class="modal-dialog modal-xl" role="document">
            <div class="modal-content">
                <div class="modal-header">
                    <h5 class="modal-title">Add tags — manual entry <span class="text-muted small">(@DriverType)</span></h5>
                    <button type="button" class="btn-close" aria-label="Close" @onclick="CloseAsync"></button>
                </div>
                <div class="modal-body">
                    <div class="table-responsive">
                        <table class="table table-sm align-middle">
                            <thead>
                                <tr>
                                    <th style="width:22%">Name</th>
                                    <th style="width:14%">Data type</th>
                                    <th style="width:14%">Access</th>
                                    <th style="width:12%">WriteIdemp.</th>
                                    <th style="width:18%">PollGroup</th>
                                    <th style="width:12%">Config</th>
                                    <th style="width:8%"></th>
                                </tr>
                            </thead>
                            <tbody>
                                @foreach (var row in _rows)
                                {
                                    <tr @key="row.Key">
                                        <td>
                                            <input class="form-control form-control-sm" placeholder="TagName"
                                                   value="@row.Name" @onchange="@(e => row.Name = e.Value?.ToString() ?? "")" />
                                            @if (row.Error is not null)
                                            {
                                                <div class="text-danger small">@row.Error</div>
                                            }
                                        </td>
                                        <td>
                                            <select class="form-select form-select-sm" value="@row.DataType"
                                                    @onchange="@(e => row.DataType = e.Value?.ToString() ?? "Float")">
                                                @foreach (var dt in DataTypes)
                                                {
                                                    <option value="@dt">@dt</option>
                                                }
                                            </select>
                                        </td>
                                        <td>
                                            <select class="form-select form-select-sm" value="@row.AccessLevel"
                                                    @onchange="@(e => row.AccessLevel = ParseAccess(e.Value))">
                                                <option value="@TagAccessLevel.Read">Read</option>
                                                <option value="@TagAccessLevel.ReadWrite">ReadWrite</option>
                                            </select>
                                        </td>
                                        <td class="text-center">
                                            <input type="checkbox" class="form-check-input" checked="@row.WriteIdempotent"
                                                   @onchange="@(e => row.WriteIdempotent = e.Value is true)" />
                                        </td>
                                        <td>
                                            <input class="form-control form-control-sm mono" placeholder="(optional)"
                                                   value="@row.PollGroupId" @onchange="@(e => row.PollGroupId = e.Value?.ToString())" />
                                        </td>
                                        <td>
                                            <button type="button" class="btn btn-sm btn-outline-secondary"
                                                    @onclick="@(() => row.Expanded = !row.Expanded)">
                                                @(row.Expanded ? "Hide" : "Configure")
                                            </button>
                                        </td>
                                        <td class="text-end">
                                            <button type="button" class="btn btn-sm btn-outline-danger"
                                                    @onclick="@(() => RemoveRow(row))" disabled="@(_rows.Count == 1)">✕</button>
                                        </td>
                                    </tr>
                                    @if (row.Expanded)
                                    {
                                        <tr @key="@(row.Key + "-cfg")">
                                            <td colspan="7">
                                                <div class="border rounded p-2 bg-body-tertiary">
                                                    @{
                                                        var editorType = TagConfigEditorMap.Resolve(DriverType);
                                                    }
                                                    @if (editorType is not null)
                                                    {
                                                        <DynamicComponent Type="editorType" Parameters="BuildEditorParameters(row)" />
                                                    }
                                                    else
                                                    {
                                                        <textarea class="form-control form-control-sm mono" rows="4"
                                                                  placeholder='{ "FullName": "tag_name.AttributeName" }'
                                                                  value="@row.TagConfig"
                                                                  @onchange="@(e => row.TagConfig = e.Value?.ToString() ?? "{}")"></textarea>
                                                        <div class="form-text">Schemaless per driver type. Validated server-side at deploy.</div>
                                                    }
                                                </div>
                                            </td>
                                        </tr>
                                    }
                                }
                            </tbody>
                        </table>
                    </div>

                    <button type="button" class="btn btn-sm btn-outline-primary" @onclick="AddRow">+ Add row</button>

                    @if (!string.IsNullOrWhiteSpace(_summary))
                    {
                        <div class="alert @(_summaryOk ? "alert-success" : "alert-warning") py-2 mt-3 mb-0">@_summary</div>
                    }
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-outline-secondary" @onclick="CloseAsync" disabled="@_busy">Cancel</button>
                    <button type="button" class="btn btn-primary" @onclick="CommitAsync" disabled="@_busy">
                        @if (_busy) { <span class="spinner-border spinner-border-sm me-1"></span> }
                        Create @_rows.Count tag@(_rows.Count == 1 ? "" : "s")
                    </button>
                </div>
            </div>
        </div>
    </div>
}

@code {
    private static readonly string[] DataTypes =
        ["Boolean", "SByte", "Byte", "Int16", "UInt16", "Int32", "UInt32",
         "Int64", "UInt64", "Float", "Double", "String", "DateTime", "Guid", "ByteString"];

    /// <summary>Whether the modal is shown. Two-way (@bind-Visible) so the modal can self-close.</summary>
    [Parameter] public bool Visible { get; set; }

    /// <summary>Raised when <see cref="Visible"/> changes (self-close). Completes the @bind-Visible contract.</summary>
    [Parameter] public EventCallback<bool> VisibleChanged { get; set; }

    /// <summary>The device the new tags are created under.</summary>
    [Parameter] public string DeviceId { get; set; } = "";

    /// <summary>The containing tag group, or null to create the tags directly under the device.</summary>
    [Parameter] public string? TagGroupId { get; set; }

    /// <summary>The owning device's driver type, used for typed TagConfig-editor dispatch.</summary>
    [Parameter] public string DriverType { get; set; } = "";

    /// <summary>Raised after at least one tag is committed so the host can refresh the tree.</summary>
    [Parameter] public EventCallback OnSaved { get; set; }

    private readonly List<RowModel> _rows = new();
    private bool _busy;
    private string? _summary;
    private bool _summaryOk;
    private bool _open;   // guards re-seeding rows on unrelated re-renders

    protected override void OnParametersSet()
    {
        if (!Visible)
        {
            _open = false;
            return;
        }
        if (_open) { return; }   // same open, re-render → preserve in-progress rows
        _open = true;
        _rows.Clear();
        _rows.Add(new RowModel());
        _summary = null;
        _summaryOk = false;
    }

    private IDictionary<string, object> BuildEditorParameters(RowModel row) => new Dictionary<string, object>
    {
        ["ConfigJson"] = row.TagConfig,
        ["ConfigJsonChanged"] = EventCallback.Factory.Create<string>(this, v => row.TagConfig = v),
        ["DriverType"] = DriverType,
        ["GetDriverConfigJson"] = (Func<string>)(() => "{}"),
    };

    private void AddRow() => _rows.Add(new RowModel());

    private void RemoveRow(RowModel row)
    {
        if (_rows.Count > 1) { _rows.Remove(row); }
    }

    private static TagAccessLevel ParseAccess(object? v)
        => Enum.TryParse<TagAccessLevel>(v?.ToString(), out var a) ? a : TagAccessLevel.Read;

    private async Task CommitAsync()
    {
        _busy = true;
        _summary = null;
        try
        {
            // --- Client-side validation first: name segment + per-driver config + intra-grid dup names. ---
            foreach (var row in _rows) { row.Error = null; }

            var seen = new HashSet<string>(RawPaths.Comparer);
            var anyClientError = false;
            foreach (var row in _rows)
            {
                var nameError = RawPaths.ValidateSegment(row.Name);
                if (nameError is not null) { row.Error = nameError; anyClientError = true; continue; }
                if (!seen.Add(row.Name)) { row.Error = "Duplicate name in this batch."; anyClientError = true; continue; }
                var configError = TagConfigValidator.Validate(DriverType, row.TagConfig);
                if (configError is not null) { row.Error = configError; anyClientError = true; }
            }
            if (anyClientError)
            {
                _summaryOk = false;
                _summary = "Fix the highlighted rows and try again.";
                return;
            }

            // --- Commit each row; keep failures (with their error), drop committed rows. ---
            var created = 0;
            var failed = 0;
            foreach (var row in _rows)
            {
                var input = new RawTagInput(
                    row.Name,
                    row.DataType,
                    row.AccessLevel,
                    row.WriteIdempotent,
                    string.IsNullOrWhiteSpace(row.PollGroupId) ? null : row.PollGroupId,
                    string.IsNullOrWhiteSpace(row.TagConfig) ? "{}" : row.TagConfig);

                var result = await Svc.CreateTagAsync(DeviceId, TagGroupId, input);
                if (result.Ok)
                {
                    row.Committed = true;
                    created++;
                }
                else
                {
                    row.Error = result.Error;
                    failed++;
                }
            }

            _rows.RemoveAll(r => r.Committed);

            if (failed == 0)
            {
                if (created > 0) { await OnSaved.InvokeAsync(); }
                await CloseAsync();
            }
            else
            {
                // Some succeeded, some failed: refresh the tree for the good ones, keep the modal open on
                // the failed rows so the operator can correct them without re-entering the whole batch.
                if (created > 0) { await OnSaved.InvokeAsync(); }
                _summaryOk = false;
                _summary = $"{created} tag(s) created; {failed} failed. Fix the highlighted rows and retry.";
            }
        }
        finally
        {
            _busy = false;
        }
    }

    private async Task CloseAsync()
    {
        Visible = false;
        _open = false;
        await VisibleChanged.InvokeAsync(false);
    }

    private sealed class RowModel
    {
        public string Key { get; } = Guid.NewGuid().ToString("N");
        public string Name { get; set; } = "";
        public string DataType { get; set; } = "Float";
        public TagAccessLevel AccessLevel { get; set; } = TagAccessLevel.Read;
        public bool WriteIdempotent { get; set; }
        public string? PollGroupId { get; set; }
        public string TagConfig { get; set; } = "{}";
        public bool Expanded { get; set; }
        public bool Committed { get; set; }
        public string? Error { get; set; }
    }
}
