@* Staged CSV tag import for a Raw-tree Device / TagGroup (B2-WP5).

   Flow: upload a file (InputFile) → parse via the T0-2 CsvParser + the per-driver CsvColumnMap →
   a REVIEW GRID with a per-row verdict (OK / the validation error) and, per row, which columns were
   typed vs. taken from the TagConfigJson fallback → Commit, which calls IRawTreeService.ImportTagsAsync.
   ImportTagsAsync is all-or-nothing, so Commit is BLOCKED while any row is invalid — no partial commit.

   Modal contract (the RawTree coordinator wires this in place of the OnImportCsv stub):
     <RawCsvImportModal @bind-Visible="_importVisible"
                        DeviceId="@_importDeviceId"          (may be blank if TagGroupId is supplied)
                        TagGroupId="@_importTagGroupId"      (target group; its path is prepended to every row)
                        DriverType="@_importDriverType"
                        OnSaved="ReloadNodeChildren" />
   For a Device node: DeviceId = node.EntityId, TagGroupId = null.
   For a TagGroup node: TagGroupId = node.EntityId (DeviceId is resolved from it if not passed). *@
@using Microsoft.AspNetCore.Components.Forms
@using ZB.MOM.WW.OtOpcUa.AdminUI.Uns
@inject IRawTreeService Svc
@inject RawTagCsvExportReader ExportReader

@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">Import tags — CSV</h5>
                    <button type="button" class="btn-close" aria-label="Close" @onclick="Close"></button>
                </div>
                <div class="modal-body">
                    @if (!string.IsNullOrEmpty(_fatal))
                    {
                        <div class="alert alert-danger">@_fatal</div>
                    }

                    @if (_result is null)
                    {
                        <p class="text-muted">
                            Upload a CSV whose header row carries the columns for a
                            <span class="mono">@DriverType</span> device.
                            Target: <span class="mono">@TargetLabel</span>.
                        </p>
                        <InputFile OnChange="OnFileSelected" accept=".csv,text/csv" class="form-control" />
                        @if (_parsing)
                        {
                            <p class="text-muted mt-2"><span class="spinner-border spinner-border-sm"></span> Parsing…</p>
                        }
                    }
                    else
                    {
                        <div class="d-flex justify-content-between align-items-center mb-2">
                            <div>
                                <span class="badge bg-secondary">@_result.Rows.Count rows</span>
                                <span class="badge bg-success">@OkCount valid</span>
                                @if (ErrorCount > 0)
                                {
                                    <span class="badge bg-danger">@ErrorCount invalid</span>
                                }
                            </div>
                            <button type="button" class="btn btn-sm btn-outline-secondary" @onclick="Reset">Choose another file</button>
                        </div>

                        @if (ErrorCount > 0)
                        {
                            <div class="alert alert-warning py-1">
                                Import is all-or-nothing — fix or remove the invalid rows before committing.
                            </div>
                        }

                        <div class="table-responsive" style="max-height:50vh;overflow:auto">
                            <table class="table table-sm table-hover align-middle">
                                <thead class="sticky-top bg-body">
                                    <tr>
                                        <th style="width:2.5rem"></th>
                                        <th>Row</th>
                                        <th>Name</th>
                                        <th>Group</th>
                                        <th>Verdict</th>
                                        <th>Provenance</th>
                                        <th style="width:2.5rem"></th>
                                    </tr>
                                </thead>
                                <tbody>
                                    @foreach (var row in _result.Rows)
                                    {
                                        if (_removed.Contains(row.RowNumber)) { continue; }
                                        <tr class="@(row.Ok ? "" : "table-danger")">
                                            <td>@(row.Ok ? "✓" : "✕")</td>
                                            <td class="mono">@row.RowNumber</td>
                                            <td class="mono">@row.Name</td>
                                            <td class="mono small">@(row.EffectiveGroupPath.Length == 0 ? "(device root)" : row.EffectiveGroupPath)</td>
                                            <td>
                                                @if (row.Ok)
                                                {
                                                    <span class="text-success">OK</span>
                                                }
                                                else
                                                {
                                                    <span class="text-danger">@row.Error</span>
                                                }
                                            </td>
                                            <td class="small">
                                                @if (row.TypedColumnsUsed.Count > 0)
                                                {
                                                    <span class="text-muted">typed: </span>
                                                    <span class="mono">@string.Join(", ", row.TypedColumnsUsed)</span>
                                                }
                                                @if (row.FallbackKeys.Count > 0)
                                                {
                                                    <br /><span class="text-muted">TagConfigJson: </span>
                                                    <span class="mono">@string.Join(", ", row.FallbackKeys)</span>
                                                }
                                                @if (row.TypedOverrodeFallback.Count > 0)
                                                {
                                                    <br /><span class="badge bg-info text-dark" title="A typed column overrode the same key in TagConfigJson">
                                                        typed wins: @string.Join(", ", row.TypedOverrodeFallback)
                                                    </span>
                                                }
                                            </td>
                                            <td>
                                                <button type="button" class="btn btn-sm btn-outline-danger py-0"
                                                        title="Remove this row from the import"
                                                        @onclick="() => RemoveRow(row.RowNumber)">✕</button>
                                            </td>
                                        </tr>
                                    }
                                </tbody>
                            </table>
                        </div>

                        @if (_commitErrors.Count > 0)
                        {
                            <div class="alert alert-danger mt-2">
                                <strong>Commit rejected:</strong>
                                <ul class="mb-0">
                                    @foreach (var e in _commitErrors)
                                    {
                                        <li>@e</li>
                                    }
                                </ul>
                            </div>
                        }
                    }
                </div>
                <div class="modal-footer">
                    <button type="button" class="btn btn-outline-secondary" @onclick="Close">Cancel</button>
                    <button type="button" class="btn btn-primary"
                            disabled="@(!CanCommit || _committing)"
                            @onclick="Commit">
                        @if (_committing)
                        {
                            <span class="spinner-border spinner-border-sm"></span>
                        }
                        Commit @RemainingCount tag@(RemainingCount == 1 ? "" : "s")
                    </button>
                </div>
            </div>
        </div>
    </div>
}

@code {
    /// <summary>Whether the modal is shown (two-way bound).</summary>
    [Parameter] public bool Visible { get; set; }

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

    /// <summary>The target device's logical id. May be blank when <see cref="TagGroupId"/> is supplied
    /// (the device is then resolved from the group).</summary>
    [Parameter] public string DeviceId { get; set; } = "";

    /// <summary>The target tag-group's logical id, or null to import at the device root. Its device-relative
    /// path is prepended to every imported row's <c>TagGroupPath</c>.</summary>
    [Parameter] public string? TagGroupId { get; set; }

    /// <summary>The device's driver-type string — selects the per-driver typed column set.</summary>
    [Parameter] public string DriverType { get; set; } = "";

    /// <summary>Raised after a successful commit so the coordinator can reload the node's children.</summary>
    [Parameter] public EventCallback OnSaved { get; set; }

    private RawCsvParseResult? _result;
    private readonly HashSet<int> _removed = new();
    private List<string> _commitErrors = new();
    private string? _fatal;
    private bool _parsing;
    private bool _committing;
    private bool _resolved;

    private string _effectiveDeviceId = "";
    private string? _groupPrefix;

    private string TargetLabel =>
        string.IsNullOrEmpty(_groupPrefix) ? _effectiveDeviceId : $"{_effectiveDeviceId} / {_groupPrefix}";

    private int OkCount => _result?.Rows.Count(r => r.Ok && !_removed.Contains(r.RowNumber)) ?? 0;
    private int ErrorCount => _result?.Rows.Count(r => !r.Ok && !_removed.Contains(r.RowNumber)) ?? 0;
    private int RemainingCount => _result?.Rows.Count(r => !_removed.Contains(r.RowNumber)) ?? 0;

    // Commit is unblocked only when every remaining row is valid AND at least one row remains.
    private bool CanCommit =>
        _result is not null && _fatal is null && RemainingCount > 0 && ErrorCount == 0;

    protected override async Task OnParametersSetAsync()
    {
        if (Visible && !_resolved)
        {
            await ResolveTargetAsync();
            _resolved = true;
        }
        else if (!Visible)
        {
            _resolved = false;
        }
    }

    // Resolve the effective device id + the group-path prefix once per open.
    private async Task ResolveTargetAsync()
    {
        _effectiveDeviceId = DeviceId;
        _groupPrefix = null;

        if (!string.IsNullOrEmpty(TagGroupId))
        {
            var ctx = await ExportReader.ResolveGroupContextAsync(TagGroupId!);
            if (ctx is { } c)
            {
                _groupPrefix = c.GroupPath;
                if (string.IsNullOrEmpty(_effectiveDeviceId))
                {
                    _effectiveDeviceId = c.DeviceId;
                }
            }
        }
    }

    private async Task OnFileSelected(InputFileChangeEventArgs e)
    {
        _fatal = null;
        _commitErrors = new();
        _removed.Clear();
        _parsing = true;

        try
        {
            using var stream = e.File.OpenReadStream(maxAllowedSize: 16 * 1024 * 1024);
            using var reader = new StreamReader(stream);
            var text = await reader.ReadToEndAsync();
            _result = RawTagCsvMapper.Parse(DriverType, text, _groupPrefix);
            _fatal = _result.FatalError;
        }
        catch (Exception ex)
        {
            _fatal = $"Could not read the file: {ex.Message}";
            _result = null;
        }
        finally
        {
            _parsing = false;
        }
    }

    private void RemoveRow(int rowNumber) => _removed.Add(rowNumber);

    private void Reset()
    {
        _result = null;
        _fatal = null;
        _commitErrors = new();
        _removed.Clear();
    }

    private async Task Commit()
    {
        if (_result is null || !CanCommit)
        {
            return;
        }

        _committing = true;
        _commitErrors = new();
        try
        {
            var rows = _result.Rows
                .Where(r => r is { Ok: true, ImportRow: not null } && !_removed.Contains(r.RowNumber))
                .Select(r => r.ImportRow!)
                .ToList();

            var outcome = await Svc.ImportTagsAsync(_effectiveDeviceId, rows);
            if (outcome.Errors.Count > 0)
            {
                _commitErrors = outcome.Errors.ToList();
                return;
            }

            await OnSaved.InvokeAsync();
            await Close();
        }
        catch (Exception ex)
        {
            _commitErrors = new() { ex.Message };
        }
        finally
        {
            _committing = false;
        }
    }

    private async Task Close()
    {
        Reset();
        Visible = false;
        _resolved = false;
        await VisibleChanged.InvokeAsync(false);
    }
}
