@page "/clusters/{ClusterId}/drivers/new/s7"
@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.S7
@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 Siemens S7 driver" : "Edit Siemens S7 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="s7DriverEdit">
        <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="S7 address"
                             CurrentAddress="@_pickedAddress"
                             OnPickAddress="@OnAddressPicked">
                <S7AddressPickerBody CurrentAddress="@_pickedAddress"
                                     CurrentAddressChanged="@((s) => _pickedAddress = s)" />
            </DriverTagPicker>

            @* Connection *@
            <section class="panel rise mt-3" style="animation-delay:.05s">
                <div class="panel-head">Connection</div>
                <div style="padding:1rem">
                    <div class="row">
                        <div class="col-md-6 mb-3">
                            <label class="form-label" for="s7Host">Host</label>
                            <InputText id="s7Host" @bind-Value="_form.Host"
                                       class="form-control form-control-sm mono"
                                       placeholder="192.168.0.1" />
                            <div class="form-text">PLC IP address or hostname.</div>
                        </div>
                        <div class="col-md-3 mb-3">
                            <label class="form-label" for="s7Port">Port</label>
                            <InputNumber id="s7Port" @bind-Value="_form.Port"
                                         class="form-control form-control-sm" />
                            <div class="form-text">ISO-on-TCP; usually 102.</div>
                        </div>
                        <div class="col-md-3 mb-3">
                            <label class="form-label" for="s7TimeoutSec">Timeout (seconds)</label>
                            <InputNumber id="s7TimeoutSec" @bind-Value="_form.TimeoutSeconds"
                                         class="form-control form-control-sm" />
                        </div>
                    </div>
                    <div class="row">
                        <div class="col-md-4 mb-3">
                            <label class="form-label" for="s7CpuType">CPU type</label>
                            <InputSelect id="s7CpuType" @bind-Value="_form.CpuType"
                                         class="form-select form-select-sm">
                                @foreach (var v in Enum.GetValues<S7CpuType>())
                                {
                                    <option value="@v">@v</option>
                                }
                            </InputSelect>
                            <div class="form-text">Controls ISO-TSAP slot byte during handshake.</div>
                        </div>
                        <div class="col-md-2 mb-3">
                            <label class="form-label" for="s7Rack">Rack</label>
                            <InputNumber id="s7Rack" @bind-Value="_form.Rack"
                                         class="form-control form-control-sm" />
                            <div class="form-text">Almost always 0.</div>
                        </div>
                        <div class="col-md-2 mb-3">
                            <label class="form-label" for="s7Slot">Slot</label>
                            <InputNumber id="s7Slot" @bind-Value="_form.Slot"
                                         class="form-control form-control-sm" />
                            <div class="form-text">S7-300/400 = 2; S7-1200/1500 = 0.</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="s7ProbeEnabled" @bind-Value="_form.ProbeEnabled"
                                               class="form-check-input" />
                                <label class="form-check-label" for="s7ProbeEnabled">Probe enabled</label>
                            </div>
                        </div>
                        <div class="col-md-3 mb-3">
                            <label class="form-label" for="s7ProbeIntervalSec">Probe interval (s)</label>
                            <InputNumber id="s7ProbeIntervalSec" @bind-Value="_form.ProbeIntervalSeconds"
                                         class="form-control form-control-sm" />
                        </div>
                        <div class="col-md-3 mb-3">
                            <label class="form-label" for="s7ProbeTimeoutSec">Probe timeout (s)</label>
                            <InputNumber id="s7ProbeTimeoutSec" @bind-Value="_form.ProbeTimeoutSeconds"
                                         class="form-control form-control-sm" />
                        </div>
                        <div class="col-md-3 mb-3">
                            <label class="form-label" for="s7AdminProbeTimeout">Admin probe timeout (s)</label>
                            <InputNumber id="s7AdminProbeTimeout" @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>

            @* Tags *@
            <CollectionEditor TRow="S7TagRow" Items="_tags" Title="Tags" ItemNoun="tag"
                              AnimationDelay=".11s"
                              NewRow="@(() => new S7TagRow())" Clone="@(r => r.Clone())"
                              Validate="S7TagRow.ValidateRow">
                <HeaderTemplate>
                    <tr><th>Name</th><th>Address</th><th>Type</th><th>Writable</th><th></th></tr>
                </HeaderTemplate>
                <RowTemplate Context="t">
                    <td class="mono">@t.Name</td><td class="mono">@t.Address</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">Address</label>
                            <input class="form-control form-control-sm mono" @bind="t.Address"
                                   placeholder="e.g. DB1.DBW0, M0.0, I0.0, QD4" /></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<S7DataType>()) { <option value="@e">@e</option> }
                            </select></div>
                        <div class="col-md-3"><label class="form-label">String length</label>
                            <input type="number" class="form-control form-control-sm" @bind="t.StringLength" />
                            <div class="form-text">Only for String type. Max 254.</div></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 = "S7";

    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 Tags is a collection — edited via the CollectionEditor modal.
    private List<S7TagRow> _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 S7DriverOptions());
        }
        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 S7DriverOptions();
                _form = FormModel.FromOptions(opts);
                _form.ResilienceConfig = _existing.ResilienceConfig;
                _form.RowVersion = _existing.RowVersion;
                _tags = opts.Tags.Select(S7TagRow.FromDefinition).ToList();
            }
        }
        _loaded = true;
    }

    private async Task SubmitAsync()
    {
        _busy = true; _error = null;
        try
        {
            var opts = _form.ToOptions(_tags.Select(r => r.ToDefinition()).ToList());
            var configJson = System.Text.Json.JsonSerializer.Serialize(opts, _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(_tags.Select(r => r.ToDefinition()).ToList()), _jsonOpts);

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

    // Mutable VM for the modal editor — S7TagDefinition is an immutable record.
    public sealed class S7TagRow
    {
        public string Name { get; set; } = "";
        public string Address { get; set; } = "";
        public S7DataType DataType { get; set; } = S7DataType.Int16;
        public bool Writable { get; set; } = true;
        public int StringLength { get; set; } = 254;

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

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

        public static S7TagRow FromDefinition(S7TagDefinition d) => new()
        {
            Name = d.Name, Address = d.Address, DataType = d.DataType,
            Writable = d.Writable, StringLength = d.StringLength,
            _source = d,
        };

        public S7TagDefinition ToDefinition()
        {
            var baseDef = _source ?? new S7TagDefinition(Name.Trim(), Address.Trim(), DataType);
            return baseDef with
            {
                Name = Name.Trim(),
                Address = Address.Trim(),
                DataType = DataType,
                Writable = Writable,
                StringLength = StringLength,
            };
        }

        public static string? ValidateRow(S7TagRow row, IReadOnlyList<S7TagRow> 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.
    // Collection (Tags) is kept on the component (_tags) and passed in on ToOptions().
    public sealed class FormModel
    {
        // Connection
        public string Host { get; set; } = "127.0.0.1";
        public int Port { get; set; } = 102;
        public S7CpuType CpuType { get; set; } = S7CpuType.S71500;
        public short Rack { get; set; } = 0;
        public short Slot { get; set; } = 0;
        public int TimeoutSeconds { get; set; } = 5;

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

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

        public static FormModel FromOptions(S7DriverOptions o) => new()
        {
            Host = o.Host,
            Port = o.Port,
            CpuType = o.CpuType,
            Rack = o.Rack,
            Slot = o.Slot,
            TimeoutSeconds = (int)o.Timeout.TotalSeconds,
            ProbeEnabled = o.Probe.Enabled,
            ProbeIntervalSeconds = (int)o.Probe.Interval.TotalSeconds,
            ProbeTimeoutSeconds = (int)o.Probe.Timeout.TotalSeconds,
            AdminProbeTimeoutSeconds = o.ProbeTimeoutSeconds,
        };

        public S7DriverOptions ToOptions(IReadOnlyList<S7TagDefinition> tags) => new()
        {
            Host = Host,
            Port = Port,
            CpuType = CpuType,
            Rack = Rack,
            Slot = Slot,
            Timeout = TimeSpan.FromSeconds(TimeoutSeconds),
            Probe = new S7ProbeOptions
            {
                Enabled = ProbeEnabled,
                Interval = TimeSpan.FromSeconds(ProbeIntervalSeconds),
                Timeout = TimeSpan.FromSeconds(ProbeTimeoutSeconds),
            },
            ProbeTimeoutSeconds = AdminProbeTimeoutSeconds,
            Tags = tags,
        };
    }
}
