@page "/clusters/{ClusterId}/drivers/new/twincat"
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
@rendermode RenderMode.InteractiveServer
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.EntityFrameworkCore
@using ZB.MOM.WW.OtOpcUa.AdminUI.Clients
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers.Pickers
@using ZB.MOM.WW.OtOpcUa.Configuration
@using ZB.MOM.WW.OtOpcUa.Configuration.Entities
@using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT
@inject IDbContextFactory<OtOpcUaConfigDbContext> DbFactory
@inject NavigationManager Nav

<div class="d-flex justify-content-between align-items-center mb-3">
    <h4 class="mb-0">@(IsNew ? "New Beckhoff TwinCAT driver" : "Edit Beckhoff TwinCAT driver") &middot; <span class="mono">@ClusterId</span></h4>
    <a href="/clusters/@ClusterId/drivers" class="btn btn-outline-secondary btn-sm">Cancel</a>
</div>
<ClusterNav ClusterId="@ClusterId" ActiveTab="drivers" />

@if (!_loaded)
{
    <p>Loading…</p>
}
else if (!IsNew && _existing is null)
{
    <section class="panel notice rise" style="animation-delay:.02s">
        Driver instance <span class="mono">@DriverInstanceId</span> was not found in cluster <span class="mono">@ClusterId</span>.
    </section>
}
else
{
    <EditForm Model="_form" OnValidSubmit="SubmitAsync" FormName="twincatDriverEdit">
        <DataAnnotationsValidator />
        <DriverFormShell IsNew="IsNew" Busy="_busy" Error="_error"
                         CancelHref="@($"/clusters/{ClusterId}/drivers")"
                         OnDelete="@(IsNew ? null : (EventCallback?)EventCallback.Factory.Create(this, DeleteAsync))">

            <DriverIdentitySection Model="_identityModel" Namespaces="_namespaces" IsNew="IsNew" ShowDriverType="false" />

            @if (!IsNew && !string.IsNullOrEmpty(DriverInstanceId))
            {
                <DriverStatusPanel DriverInstanceId="@DriverInstanceId" Enabled="@_identityModel.Enabled" />
            }

            <div class="mt-2 mb-3">
                <DriverTestConnectButton DriverType="@DriverTypeKey"
                                         GetConfigJson="@SerializeCurrentConfig"
                                         TimeoutSeconds="@_form.AdminProbeTimeoutSeconds" />
                <button type="button" class="btn btn-sm btn-outline-secondary mt-2"
                        @onclick="@(() => _showPicker = true)">
                    Pick address
                </button>
            </div>

            <DriverTagPicker @bind-Visible="_showPicker"
                             Title="TwinCAT address"
                             CurrentAddress="@_pickedAddress"
                             OnPickAddress="@OnAddressPicked">
                <TwinCATAddressPickerBody CurrentAddress="@_pickedAddress"
                                          CurrentAddressChanged="@((s) => _pickedAddress = s)" />
            </DriverTagPicker>

            @* Options *@
            <section class="panel rise mt-3" style="animation-delay:.05s">
                <div class="panel-head">Options</div>
                <div style="padding:1rem">
                    <div class="row">
                        <div class="col-md-3 mb-3">
                            <label class="form-label" for="tcTimeoutSec">Timeout (seconds)</label>
                            <InputNumber id="tcTimeoutSec" @bind-Value="_form.TimeoutSeconds"
                                         class="form-control form-control-sm" />
                            <div class="form-text">Default 2 s per operation.</div>
                        </div>
                        <div class="col-md-4 mb-3">
                            <div class="form-check form-switch mt-4">
                                <InputCheckbox id="tcNativeNotif" @bind-Value="_form.UseNativeNotifications"
                                               class="form-check-input" />
                                <label class="form-check-label" for="tcNativeNotif">Use native ADS notifications</label>
                            </div>
                            <div class="form-text">Recommended. Disable for AMS routers with notification limits.</div>
                        </div>
                        <div class="col-md-4 mb-3">
                            <div class="form-check form-switch mt-4">
                                <InputCheckbox id="tcControllerBrowse" @bind-Value="_form.EnableControllerBrowse"
                                               class="form-check-input" />
                                <label class="form-check-label" for="tcControllerBrowse">Enable controller browse</label>
                            </div>
                            <div class="form-text">Walks the symbol table via SymbolLoaderFactory at discovery. Default off.</div>
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-md-4 mb-3">
                            <label class="form-label" for="tcNotifDelay">Notification max delay (ms)</label>
                            <InputNumber id="tcNotifDelay" @bind-Value="_form.NotificationMaxDelayMs"
                                         class="form-control form-control-sm" />
                            <div class="form-text">0 = push immediately. Increase to coalesce high-churn signals.</div>
                        </div>
                    </div>
                </div>
            </section>

            @* Probe *@
            <section class="panel rise mt-3" style="animation-delay:.08s">
                <div class="panel-head">Connectivity probe</div>
                <div style="padding:1rem">
                    <div class="row">
                        <div class="col-md-3 mb-3">
                            <div class="form-check form-switch mt-2">
                                <InputCheckbox id="tcProbeEnabled" @bind-Value="_form.ProbeEnabled"
                                               class="form-check-input" />
                                <label class="form-check-label" for="tcProbeEnabled">Probe enabled</label>
                            </div>
                        </div>
                        <div class="col-md-3 mb-3">
                            <label class="form-label" for="tcProbeInterval">Probe interval (s)</label>
                            <InputNumber id="tcProbeInterval" @bind-Value="_form.ProbeIntervalSeconds"
                                         class="form-control form-control-sm" />
                        </div>
                        <div class="col-md-3 mb-3">
                            <label class="form-label" for="tcProbeTimeout">Probe timeout (s)</label>
                            <InputNumber id="tcProbeTimeout" @bind-Value="_form.ProbeTimeoutSeconds"
                                         class="form-control form-control-sm" />
                        </div>
                        <div class="col-md-3 mb-3">
                            <label class="form-label" for="tcAdminProbe">Admin probe timeout (s)</label>
                            <InputNumber id="tcAdminProbe" @bind-Value="_form.AdminProbeTimeoutSeconds"
                                         class="form-control form-control-sm" />
                            <div class="form-text">Test Connect timeout (1–60 s).</div>
                        </div>
                    </div>
                </div>
            </section>

            @* Devices *@
            <CollectionEditor TRow="TwinCATDeviceRow" Items="_devices" Title="Devices" ItemNoun="device"
                              AnimationDelay=".11s"
                              NewRow="@(() => new TwinCATDeviceRow())" Clone="@(r => r.Clone())"
                              Validate="TwinCATDeviceRow.ValidateRow">
                <HeaderTemplate>
                    <tr><th>Host address</th><th>Device name</th><th></th></tr>
                </HeaderTemplate>
                <RowTemplate Context="d">
                    <td class="mono">@d.HostAddress</td>
                    <td>@(string.IsNullOrWhiteSpace(d.DeviceName) ? "—" : d.DeviceName)</td>
                </RowTemplate>
                <EditTemplate Context="d">
                    <div class="row g-3">
                        <div class="col-md-6"><label class="form-label">Host address (AMS Net Id:port)</label>
                            <input class="form-control form-control-sm mono" @bind="d.HostAddress"
                                   placeholder="192.168.0.1.1.1:851" /></div>
                        <div class="col-md-6"><label class="form-label">Device name</label>
                            <input class="form-control form-control-sm" @bind="d.DeviceName" /></div>
                    </div>
                </EditTemplate>
            </CollectionEditor>

            @* Tags *@
            <CollectionEditor TRow="TwinCATTagRow" Items="_tags" Title="Tags" ItemNoun="tag"
                              AnimationDelay=".14s"
                              NewRow="@(() => new TwinCATTagRow())" Clone="@(r => r.Clone())"
                              Validate="TwinCATTagRow.ValidateRow">
                <HeaderTemplate>
                    <tr><th>Name</th><th>Device</th><th>Symbol path</th><th>Type</th><th>Writable</th><th></th></tr>
                </HeaderTemplate>
                <RowTemplate Context="t">
                    <td class="mono">@t.Name</td><td class="mono">@t.DeviceHostAddress</td>
                    <td class="mono">@t.SymbolPath</td><td>@t.DataType</td><td>@(t.Writable ? "yes" : "no")</td>
                </RowTemplate>
                <EditTemplate Context="t">
                    <div class="row g-3">
                        <div class="col-md-6"><label class="form-label">Name</label>
                            <input class="form-control form-control-sm" @bind="t.Name" /></div>
                        <div class="col-md-6"><label class="form-label">Device host address</label>
                            <input class="form-control form-control-sm mono" @bind="t.DeviceHostAddress"
                                   placeholder="192.168.0.1.1.1:851" /></div>
                        <div class="col-md-6"><label class="form-label">Symbol path</label>
                            <input class="form-control form-control-sm mono" @bind="t.SymbolPath"
                                   placeholder="e.g. MAIN.bStart, GVL.Counter" /></div>
                        <div class="col-md-3"><label class="form-label">Data type</label>
                            <select class="form-select form-select-sm" @bind="t.DataType">
                                @foreach (var e in Enum.GetValues<TwinCATDataType>()) { <option value="@e">@e</option> }
                            </select></div>
                        <div class="col-md-3"><div class="form-check form-switch mt-4">
                            <input type="checkbox" class="form-check-input" @bind="t.Writable" id="tagWritable" />
                            <label class="form-check-label" for="tagWritable">Writable</label></div></div>
                    </div>
                </EditTemplate>
            </CollectionEditor>

            <DriverResilienceSection @bind-ResilienceConfig="_form.ResilienceConfig" />
        </DriverFormShell>
    </EditForm>
}

@code {
    [Parameter] public string ClusterId { get; set; } = "";
    [Parameter] public string? DriverInstanceId { get; set; }

    private const string DriverTypeKey = "TwinCat";

    private bool IsNew => string.IsNullOrEmpty(DriverInstanceId);

    private static readonly System.Text.Json.JsonSerializerOptions _jsonOpts = new()
    {
        PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase,
        UnmappedMemberHandling = System.Text.Json.Serialization.JsonUnmappedMemberHandling.Skip,
        WriteIndented = false,
    };

    private FormModel _form = new();
    private DriverIdentitySection.DriverIdentityModel _identityModel = new() { DriverType = DriverTypeKey };
    private DriverInstance? _existing;
    private List<Namespace> _namespaces = new();
    private bool _loaded, _busy;
    private string? _error;

    // Address picker state
    private bool _showPicker;
    private string _pickedAddress = "";

    private void OnAddressPicked(string address) => _pickedAddress = address;

    // Held separately because Devices/Tags are collections — edited via the CollectionEditor modal.
    private List<TwinCATDeviceRow> _devices = [];
    private List<TwinCATTagRow> _tags = [];

    protected override async Task OnInitializedAsync()
    {
        await using var db = await DbFactory.CreateDbContextAsync();
        _namespaces = await db.Namespaces.AsNoTracking()
            .Where(n => n.ClusterId == ClusterId)
            .OrderBy(n => n.NamespaceId).ToListAsync();

        if (IsNew)
        {
            _identityModel = new() { DriverType = DriverTypeKey, NamespaceId = _namespaces.FirstOrDefault()?.NamespaceId ?? "", Enabled = true };
            _form = FormModel.FromOptions(new TwinCATDriverOptions());
        }
        else
        {
            _existing = await db.DriverInstances.AsNoTracking()
                .FirstOrDefaultAsync(d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId);
            if (_existing is not null)
            {
                _identityModel = new()
                {
                    DriverInstanceId = _existing.DriverInstanceId,
                    Name = _existing.Name,
                    DriverType = _existing.DriverType,
                    NamespaceId = _existing.NamespaceId,
                    Enabled = _existing.Enabled,
                };
                var opts = TryDeserialize(_existing.DriverConfig) ?? new TwinCATDriverOptions();
                _form = FormModel.FromOptions(opts);
                _form.ResilienceConfig = _existing.ResilienceConfig;
                _form.RowVersion = _existing.RowVersion;
                _devices = opts.Devices.Select(TwinCATDeviceRow.FromDefinition).ToList();
                _tags = opts.Tags.Select(TwinCATTagRow.FromDefinition).ToList();
            }
        }
        _loaded = true;
    }

    private async Task SubmitAsync()
    {
        _busy = true; _error = null;
        try
        {
            var configJson = System.Text.Json.JsonSerializer.Serialize(
                _form.ToOptions(
                    _devices.Select(r => r.ToDefinition()).ToList(),
                    _tags.Select(r => r.ToDefinition()).ToList()),
                _jsonOpts);
            await using var db = await DbFactory.CreateDbContextAsync();
            if (IsNew)
            {
                if (await db.DriverInstances.AnyAsync(d => d.DriverInstanceId == _identityModel.DriverInstanceId))
                {
                    _error = $"Driver instance '{_identityModel.DriverInstanceId}' already exists."; return;
                }
                db.DriverInstances.Add(new DriverInstance
                {
                    DriverInstanceId = _identityModel.DriverInstanceId,
                    ClusterId = ClusterId,
                    NamespaceId = _identityModel.NamespaceId,
                    Name = _identityModel.Name,
                    DriverType = DriverTypeKey,
                    Enabled = _identityModel.Enabled,
                    DriverConfig = configJson,
                    ResilienceConfig = string.IsNullOrWhiteSpace(_form.ResilienceConfig) ? null : _form.ResilienceConfig,
                });
            }
            else
            {
                var entity = await db.DriverInstances.FirstOrDefaultAsync(
                    d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId);
                if (entity is null) { _error = "Row no longer exists."; return; }
                db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion;
                entity.NamespaceId = _identityModel.NamespaceId;
                entity.Name = _identityModel.Name;
                entity.Enabled = _identityModel.Enabled;
                entity.DriverConfig = configJson;
                entity.ResilienceConfig = string.IsNullOrWhiteSpace(_form.ResilienceConfig) ? null : _form.ResilienceConfig;
            }
            await db.SaveChangesAsync();
            Nav.NavigateTo($"/clusters/{ClusterId}/drivers");
        }
        catch (DbUpdateConcurrencyException)
        {
            _error = "Another user changed this driver instance while you were editing. Reload to see the latest values, then re-apply your changes.";
        }
        catch (Exception ex) { _error = ex.Message; }
        finally { _busy = false; }
    }

    private async Task DeleteAsync()
    {
        if (IsNew) return;
        _busy = true; _error = null;
        try
        {
            await using var db = await DbFactory.CreateDbContextAsync();
            var entity = await db.DriverInstances.FirstOrDefaultAsync(
                d => d.ClusterId == ClusterId && d.DriverInstanceId == DriverInstanceId);
            if (entity is null) { Nav.NavigateTo($"/clusters/{ClusterId}/drivers"); return; }
            db.Entry(entity).Property(e => e.RowVersion).OriginalValue = _form.RowVersion;
            db.DriverInstances.Remove(entity);
            await db.SaveChangesAsync();
            Nav.NavigateTo($"/clusters/{ClusterId}/drivers");
        }
        catch (DbUpdateConcurrencyException)
        {
            _error = "Another user changed this driver instance while you were viewing it. Reload before deleting.";
        }
        catch (Exception ex)
        {
            _error = $"Delete failed: {ex.Message}. (Likely because equipment/tags still reference this driver — remove them first.)";
        }
        finally { _busy = false; }
    }

    private string SerializeCurrentConfig()
        => System.Text.Json.JsonSerializer.Serialize(
            _form.ToOptions(
                _devices.Select(r => r.ToDefinition()).ToList(),
                _tags.Select(r => r.ToDefinition()).ToList()),
            _jsonOpts);

    private static TwinCATDriverOptions? TryDeserialize(string json)
    {
        try { return System.Text.Json.JsonSerializer.Deserialize<TwinCATDriverOptions>(json, _jsonOpts); }
        catch { return null; }
    }

    // Mutable VM for the modal editor — TwinCATDeviceOptions is an immutable record.
    public sealed class TwinCATDeviceRow
    {
        public string HostAddress { get; set; } = "";
        public string? DeviceName { get; set; }

        // Original record (null for newly-added rows). Preserves any fields the editor doesn't
        // expose across a load→save.
        private TwinCATDeviceOptions? _source;

        public TwinCATDeviceRow Clone() => (TwinCATDeviceRow)MemberwiseClone(); // _source is an immutable record ref — safe to share

        public static TwinCATDeviceRow FromDefinition(TwinCATDeviceOptions d) => new()
        {
            HostAddress = d.HostAddress, DeviceName = d.DeviceName,
            _source = d,
        };

        public TwinCATDeviceOptions ToDefinition()
        {
            var baseDef = _source ?? new TwinCATDeviceOptions(HostAddress.Trim());
            return baseDef with
            {
                HostAddress = HostAddress.Trim(),
                DeviceName = string.IsNullOrWhiteSpace(DeviceName) ? null : DeviceName.Trim(),
            };
        }

        public static string? ValidateRow(TwinCATDeviceRow row, IReadOnlyList<TwinCATDeviceRow> all, int? editIndex)
        {
            if (string.IsNullOrWhiteSpace(row.HostAddress)) return "Host address is required.";
            for (var i = 0; i < all.Count; i++)
                if (i != editIndex && string.Equals(all[i].HostAddress, row.HostAddress, StringComparison.OrdinalIgnoreCase))
                    return $"Duplicate device host address '{row.HostAddress}'.";
            return null;
        }
    }

    // Mutable VM for the modal editor — TwinCATTagDefinition is an immutable record.
    public sealed class TwinCATTagRow
    {
        public string Name { get; set; } = "";
        public string DeviceHostAddress { get; set; } = "";
        public string SymbolPath { get; set; } = "";
        public TwinCATDataType DataType { get; set; } = TwinCATDataType.DInt;
        public bool Writable { get; set; } = true;

        // Original record (null for newly-added rows). Preserves fields the editor doesn't expose
        // (WriteIdempotent) across a load→save.
        private TwinCATTagDefinition? _source;

        public TwinCATTagRow Clone() => (TwinCATTagRow)MemberwiseClone(); // _source is an immutable record ref — safe to share

        public static TwinCATTagRow FromDefinition(TwinCATTagDefinition d) => new()
        {
            Name = d.Name, DeviceHostAddress = d.DeviceHostAddress, SymbolPath = d.SymbolPath,
            DataType = d.DataType, Writable = d.Writable,
            _source = d,
        };

        public TwinCATTagDefinition ToDefinition()
        {
            var baseDef = _source ?? new TwinCATTagDefinition(Name.Trim(), DeviceHostAddress.Trim(), SymbolPath.Trim(), DataType);
            return baseDef with
            {
                Name = Name.Trim(),
                DeviceHostAddress = DeviceHostAddress.Trim(),
                SymbolPath = SymbolPath.Trim(),
                DataType = DataType,
                Writable = Writable,
            };
        }

        public static string? ValidateRow(TwinCATTagRow row, IReadOnlyList<TwinCATTagRow> all, int? editIndex)
        {
            if (string.IsNullOrWhiteSpace(row.Name)) return "Name is required.";
            for (var i = 0; i < all.Count; i++)
                if (i != editIndex && string.Equals(all[i].Name, row.Name, StringComparison.OrdinalIgnoreCase))
                    return $"Duplicate tag name '{row.Name}'.";
            return null;
        }
    }

    // Flat mutable model — all scalar properties settable for Blazor @bind-Value.
    // Collections (Devices, Tags) are kept on the component and passed in on ToOptions().
    public sealed class FormModel
    {
        // Options
        public int TimeoutSeconds { get; set; } = 2;
        public bool UseNativeNotifications { get; set; } = true;
        public bool EnableControllerBrowse { get; set; } = false;
        public int NotificationMaxDelayMs { get; set; } = 0;

        // Probe
        public bool ProbeEnabled { get; set; } = true;
        public int ProbeIntervalSeconds { get; set; } = 5;
        public int ProbeTimeoutSeconds { get; set; } = 2;
        public int AdminProbeTimeoutSeconds { get; set; } = 10;

        // Common
        public string? ResilienceConfig { get; set; }
        public byte[] RowVersion { get; set; } = [];

        public static FormModel FromOptions(TwinCATDriverOptions o) => new()
        {
            TimeoutSeconds = (int)o.Timeout.TotalSeconds,
            UseNativeNotifications = o.UseNativeNotifications,
            EnableControllerBrowse = o.EnableControllerBrowse,
            NotificationMaxDelayMs = o.NotificationMaxDelayMs,
            ProbeEnabled = o.Probe.Enabled,
            ProbeIntervalSeconds = (int)o.Probe.Interval.TotalSeconds,
            ProbeTimeoutSeconds = (int)o.Probe.Timeout.TotalSeconds,
            AdminProbeTimeoutSeconds = o.ProbeTimeoutSeconds,
        };

        public TwinCATDriverOptions ToOptions(
            IReadOnlyList<TwinCATDeviceOptions> devices,
            IReadOnlyList<TwinCATTagDefinition> tags) => new()
        {
            Timeout = TimeSpan.FromSeconds(TimeoutSeconds),
            UseNativeNotifications = UseNativeNotifications,
            EnableControllerBrowse = EnableControllerBrowse,
            NotificationMaxDelayMs = NotificationMaxDelayMs,
            Probe = new TwinCATProbeOptions
            {
                Enabled = ProbeEnabled,
                Interval = TimeSpan.FromSeconds(ProbeIntervalSeconds),
                Timeout = TimeSpan.FromSeconds(ProbeTimeoutSeconds),
            },
            ProbeTimeoutSeconds = AdminProbeTimeoutSeconds,
            Devices = devices,
            Tags = tags,
        };
    }
}
