@* Embeddable Modbus driver (channel/protocol) config form body. Endpoint fields (Host/Port/UnitId)
   moved to ModbusDeviceForm in v3 (they live in Device.DeviceConfig). Hosted by the routed
   ModbusDriverPage and by DriverConfigModal. Exposes GetConfigJson() for the parent to pull at save +
   Test-connect time, and fires DriverConfigJsonChanged for @bind support. *@
@using System.Text.Json
@using System.Text.Json.Nodes
@using System.Text.Json.Serialization
@using ZB.MOM.WW.OtOpcUa.AdminUI.Components.Shared.Drivers
@using ZB.MOM.WW.OtOpcUa.Driver.Modbus

@* Family / transport-flags *@
<section class="panel rise mt-3" style="animation-delay:.06s">
    <div class="panel-head">Protocol</div>
    <div style="padding:1rem">
        <div class="row g-3">
            <div class="col-md-3">
                <label class="form-label">Timeout (seconds)</label>
                <InputNumber @bind-Value="_form.TimeoutSeconds" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Default 2 s.</div>
            </div>
            <div class="col-md-3">
                <label class="form-label">PLC family</label>
                <InputSelect @bind-Value="_form.Family" @bind-Value:after="EmitAsync" class="form-select form-select-sm">
                    @foreach (var e in Enum.GetValues<ModbusFamily>())
                    {
                        <option value="@e">@e</option>
                    }
                </InputSelect>
            </div>
            <div class="col-md-3">
                <label class="form-label">MELSEC sub-family</label>
                <InputSelect @bind-Value="_form.MelsecSubFamily" @bind-Value:after="EmitAsync" class="form-select form-select-sm">
                    @foreach (var e in Enum.GetValues<MelsecFamily>())
                    {
                        <option value="@e">@e</option>
                    }
                </InputSelect>
                <div class="form-text">Only used when Family = MELSEC.</div>
            </div>
            <div class="col-md-3">
                <div class="form-check form-switch mt-4">
                    <InputCheckbox @bind-Value="_form.AutoReconnect" @bind-Value:after="EmitAsync" class="form-check-input" id="autoReconnect" />
                    <label class="form-check-label" for="autoReconnect">Auto-reconnect</label>
                </div>
            </div>
        </div>
    </div>
</section>

@* Batch sizes *@
<section class="panel rise mt-3" style="animation-delay:.08s">
    <div class="panel-head">Batch sizes</div>
    <div style="padding:1rem">
        <div class="row g-3">
            <div class="col-md-3">
                <label class="form-label">Max registers per read</label>
                <InputNumber @bind-Value="_form.MaxRegistersPerRead" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Spec max 125. Reduce for limited devices.</div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Max registers per write</label>
                <InputNumber @bind-Value="_form.MaxRegistersPerWrite" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Spec max 123.</div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Max coils per read</label>
                <InputNumber @bind-Value="_form.MaxCoilsPerRead" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Spec max 2000.</div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Max read gap (coalescing)</label>
                <InputNumber @bind-Value="_form.MaxReadGap" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">0 = no coalescing. Typical 5–32.</div>
            </div>
        </div>
    </div>
</section>

@* Write options *@
<section class="panel rise mt-3" style="animation-delay:.10s">
    <div class="panel-head">Write options</div>
    <div style="padding:1rem">
        <div class="row g-3">
            <div class="col-md-4">
                <div class="form-check form-switch">
                    <InputCheckbox @bind-Value="_form.UseFC15ForSingleCoilWrites" @bind-Value:after="EmitAsync" class="form-check-input" id="useFC15" />
                    <label class="form-check-label" for="useFC15">Use FC15 for single coil writes</label>
                </div>
            </div>
            <div class="col-md-4">
                <div class="form-check form-switch">
                    <InputCheckbox @bind-Value="_form.UseFC16ForSingleRegisterWrites" @bind-Value:after="EmitAsync" class="form-check-input" id="useFC16" />
                    <label class="form-check-label" for="useFC16">Use FC16 for single register writes</label>
                </div>
            </div>
            <div class="col-md-4">
                <div class="form-check form-switch">
                    <InputCheckbox @bind-Value="_form.DisableFC23" @bind-Value:after="EmitAsync" class="form-check-input" id="disableFC23" />
                    <label class="form-check-label" for="disableFC23">Disable FC23 (reserved — no effect today)</label>
                </div>
            </div>
            <div class="col-md-4">
                <div class="form-check form-switch">
                    <InputCheckbox @bind-Value="_form.WriteOnChangeOnly" @bind-Value:after="EmitAsync" class="form-check-input" id="writeOnChangeOnly" />
                    <label class="form-check-label" for="writeOnChangeOnly">Write on change only</label>
                </div>
            </div>
        </div>
    </div>
</section>

@* Keep-alive *@
<section class="panel rise mt-3" style="animation-delay:.12s">
    <div class="panel-head">TCP keep-alive</div>
    <div style="padding:1rem">
        <div class="row g-3">
            <div class="col-md-3">
                <div class="form-check form-switch mt-4">
                    <InputCheckbox @bind-Value="_form.KeepAliveEnabled" @bind-Value:after="EmitAsync" class="form-check-input" id="kaEnabled" />
                    <label class="form-check-label" for="kaEnabled">Enabled</label>
                </div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Time (seconds)</label>
                <InputNumber @bind-Value="_form.KeepAliveTimeSeconds" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Idle time before first probe. Default 30 s.</div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Interval (seconds)</label>
                <InputNumber @bind-Value="_form.KeepAliveIntervalSeconds" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Between probes once started. Default 10 s.</div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Retry count</label>
                <InputNumber @bind-Value="_form.KeepAliveRetryCount" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Probes before declaring socket dead. Default 3.</div>
            </div>
        </div>
    </div>
</section>

@* Reconnect backoff *@
<section class="panel rise mt-3" style="animation-delay:.14s">
    <div class="panel-head">Reconnect backoff</div>
    <div style="padding:1rem">
        <div class="row g-3">
            <div class="col-md-3">
                <label class="form-label">Initial delay (seconds)</label>
                <InputNumber @bind-Value="_form.ReconnectInitialDelaySeconds" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">0 = immediate retry.</div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Max delay (seconds)</label>
                <InputNumber @bind-Value="_form.ReconnectMaxDelaySeconds" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Default 30 s.</div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Backoff multiplier</label>
                <InputNumber @bind-Value="_form.ReconnectBackoffMultiplier" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Default 2.0 (doubles each step).</div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Idle disconnect (seconds, 0 = off)</label>
                <InputNumber @bind-Value="_form.IdleDisconnectTimeoutSeconds" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Proactive close after idle period. 0 = disabled.</div>
            </div>
        </div>
    </div>
</section>

@* Probe *@
<section class="panel rise mt-3" style="animation-delay:.16s">
    <div class="panel-head">Connectivity probe</div>
    <div style="padding:1rem">
        <div class="row g-3">
            <div class="col-md-3">
                <div class="form-check form-switch mt-4">
                    <InputCheckbox @bind-Value="_form.ProbeEnabled" @bind-Value:after="EmitAsync" class="form-check-input" id="probeEnabled" />
                    <label class="form-check-label" for="probeEnabled">Enabled</label>
                </div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Probe interval (seconds)</label>
                <InputNumber @bind-Value="_form.ProbeIntervalSeconds" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Default 5 s.</div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Probe timeout (seconds)</label>
                <InputNumber @bind-Value="_form.ProbeTimeoutSeconds" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Default 2 s.</div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Probe register address</label>
                <InputNumber @bind-Value="_form.ProbeAddress" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Zero-based. Default 0 (FC03 at register 0).</div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Admin UI probe timeout (seconds)</label>
                <InputNumber @bind-Value="_form.AdminProbeTimeoutSeconds" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Max 60. Used by Test Connect. Default 5.</div>
            </div>
            <div class="col-md-3">
                <label class="form-label">Auto-prohibit re-probe interval (seconds, 0 = off)</label>
                <InputNumber @bind-Value="_form.AutoProhibitReprobeIntervalSeconds" @bind-Value:after="EmitAsync" class="form-control form-control-sm" />
                <div class="form-text">Interval to retry auto-prohibited coalesced ranges.</div>
            </div>
        </div>
    </div>
</section>

<DriverResilienceSection ResilienceConfig="@ResilienceConfig" ResilienceConfigChanged="OnResilienceChanged" />

@code {
    /// <summary>The driver-level DriverConfig JSON (protocol/channel; endpoint lives in the device).</summary>
    [Parameter] public string DriverConfigJson { get; set; } = "{}";
    /// <summary>Fired (camelCase-serialized) whenever a channel field changes — enables @bind-DriverConfigJson.</summary>
    [Parameter] public EventCallback<string> DriverConfigJsonChanged { get; set; }
    /// <summary>The per-instance resilience-pipeline overrides JSON, or null.</summary>
    [Parameter] public string? ResilienceConfig { get; set; }
    /// <summary>Fired when the resilience overrides change.</summary>
    [Parameter] public EventCallback<string?> ResilienceConfigChanged { get; set; }

    private static readonly JsonSerializerOptions _jsonOpts = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        UnmappedMemberHandling = JsonUnmappedMemberHandling.Skip,
        WriteIndented = false,
        Converters = { new JsonStringEnumConverter() },
    };

    private FormModel _form = new();
    private string? _lastParsed;

    protected override void OnParametersSet()
    {
        // Re-parse only when the inbound value actually changed (don't clobber in-progress edits).
        if (!string.Equals(_lastParsed, DriverConfigJson, StringComparison.Ordinal))
        {
            var opts = TryDeserialize(DriverConfigJson) ?? new ModbusDriverOptions();
            _form = FormModel.FromOptions(opts);
            _lastParsed = DriverConfigJson;
        }
    }

    // v3: the endpoint lives on the Device (DeviceConfig), not the driver. Strip the endpoint keys from
    // the serialized channel config so DeviceConfig is the SOLE endpoint source — otherwise a stale
    // driver-level default (host=127.0.0.1) is carried in DriverConfig and shadows the device on any
    // driver that reads a single top-level Host, silently, as soon as a 2nd device exists (reviewer H1).
    private static readonly string[] EndpointKeys = ["host", "port", "unitId"];

    /// <summary>Serializes the current channel/protocol config to camelCase JSON, with the endpoint keys
    /// removed (the device's DeviceConfig is the sole endpoint source, merged up at deploy/probe time).</summary>
    public string GetConfigJson()
    {
        var node = JsonSerializer.SerializeToNode(_form.ToOptions(), _jsonOpts)!.AsObject();
        foreach (var key in EndpointKeys) node.Remove(key);
        return node.ToJsonString(_jsonOpts);
    }

    private async Task EmitAsync()
    {
        var json = GetConfigJson();
        _lastParsed = json;
        await DriverConfigJsonChanged.InvokeAsync(json);
    }

    private async Task OnResilienceChanged(string? r)
    {
        ResilienceConfig = r;
        await ResilienceConfigChanged.InvokeAsync(r);
    }

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

    // Flat mutable model — retains Host/Port/UnitId at defaults (no UI; the device form authors the real
    // endpoint into DeviceConfig, which merges up and overrides these at deploy/probe time).
    public sealed class FormModel
    {
        // Transport endpoint (no UI — authored via the device form; kept at default so ToOptions is valid)
        public string Host { get; set; } = "127.0.0.1";
        public int Port { get; set; } = 502;
        public int UnitId { get; set; } = 1;
        public int TimeoutSeconds { get; set; } = 2;

        // Family
        public ModbusFamily Family { get; set; } = ModbusFamily.Generic;
        public MelsecFamily MelsecSubFamily { get; set; } = MelsecFamily.Q_L_iQR;

        // Transport flags
        public bool AutoReconnect { get; set; } = true;
        public int IdleDisconnectTimeoutSeconds { get; set; } = 0;

        // Batch sizes (int; clamped to ushort on ToOptions)
        public int MaxRegistersPerRead { get; set; } = 125;
        public int MaxRegistersPerWrite { get; set; } = 123;
        public int MaxCoilsPerRead { get; set; } = 2000;
        public int MaxReadGap { get; set; } = 0;

        // Write options
        public bool UseFC15ForSingleCoilWrites { get; set; } = false;
        public bool UseFC16ForSingleRegisterWrites { get; set; } = false;
        public bool DisableFC23 { get; set; } = false;
        public bool WriteOnChangeOnly { get; set; } = false;

        // Keep-alive
        public bool KeepAliveEnabled { get; set; } = true;
        public int KeepAliveTimeSeconds { get; set; } = 30;
        public int KeepAliveIntervalSeconds { get; set; } = 10;
        public int KeepAliveRetryCount { get; set; } = 3;

        // Reconnect backoff
        public int ReconnectInitialDelaySeconds { get; set; } = 0;
        public int ReconnectMaxDelaySeconds { get; set; } = 30;
        public double ReconnectBackoffMultiplier { get; set; } = 2.0;

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

        // Auto-prohibit re-probe (0 = disabled)
        public int AutoProhibitReprobeIntervalSeconds { get; set; } = 0;

        // Admin UI probe timeout
        public int AdminProbeTimeoutSeconds { get; set; } = 5;

        public static FormModel FromOptions(ModbusDriverOptions o) => new()
        {
            Host = o.Host,
            Port = o.Port,
            UnitId = o.UnitId,
            TimeoutSeconds = (int)o.Timeout.TotalSeconds,
            Family = o.Family,
            MelsecSubFamily = o.MelsecSubFamily,
            AutoReconnect = o.AutoReconnect,
            IdleDisconnectTimeoutSeconds = o.IdleDisconnectTimeout.HasValue ? (int)o.IdleDisconnectTimeout.Value.TotalSeconds : 0,
            MaxRegistersPerRead = o.MaxRegistersPerRead,
            MaxRegistersPerWrite = o.MaxRegistersPerWrite,
            MaxCoilsPerRead = o.MaxCoilsPerRead,
            MaxReadGap = o.MaxReadGap,
            UseFC15ForSingleCoilWrites = o.UseFC15ForSingleCoilWrites,
            UseFC16ForSingleRegisterWrites = o.UseFC16ForSingleRegisterWrites,
            DisableFC23 = o.DisableFC23,
            WriteOnChangeOnly = o.WriteOnChangeOnly,
            KeepAliveEnabled = o.KeepAlive.Enabled,
            KeepAliveTimeSeconds = (int)o.KeepAlive.Time.TotalSeconds,
            KeepAliveIntervalSeconds = (int)o.KeepAlive.Interval.TotalSeconds,
            KeepAliveRetryCount = o.KeepAlive.RetryCount,
            ReconnectInitialDelaySeconds = (int)o.Reconnect.InitialDelay.TotalSeconds,
            ReconnectMaxDelaySeconds = (int)o.Reconnect.MaxDelay.TotalSeconds,
            ReconnectBackoffMultiplier = o.Reconnect.BackoffMultiplier,
            ProbeEnabled = o.Probe.Enabled,
            ProbeIntervalSeconds = (int)o.Probe.Interval.TotalSeconds,
            ProbeTimeoutSeconds = (int)o.Probe.Timeout.TotalSeconds,
            ProbeAddress = o.Probe.ProbeAddress,
            AutoProhibitReprobeIntervalSeconds = o.AutoProhibitReprobeInterval.HasValue
                ? (int)o.AutoProhibitReprobeInterval.Value.TotalSeconds : 0,
            AdminProbeTimeoutSeconds = o.ProbeTimeoutSeconds,
        };

        public ModbusDriverOptions ToOptions() => new()
        {
            Host = Host,
            Port = Port,
            UnitId = (byte)Math.Clamp(UnitId, 0, 255),
            Timeout = TimeSpan.FromSeconds(TimeoutSeconds),
            Probe = new ModbusProbeOptions
            {
                Enabled = ProbeEnabled,
                Interval = TimeSpan.FromSeconds(ProbeIntervalSeconds),
                Timeout = TimeSpan.FromSeconds(ProbeTimeoutSeconds),
                ProbeAddress = (ushort)Math.Clamp(ProbeAddress, 0, 65535),
            },
            MaxRegistersPerRead = (ushort)Math.Clamp(MaxRegistersPerRead, 0, 65535),
            MaxRegistersPerWrite = (ushort)Math.Clamp(MaxRegistersPerWrite, 0, 65535),
            MaxCoilsPerRead = (ushort)Math.Clamp(MaxCoilsPerRead, 0, 65535),
            UseFC15ForSingleCoilWrites = UseFC15ForSingleCoilWrites,
            UseFC16ForSingleRegisterWrites = UseFC16ForSingleRegisterWrites,
            DisableFC23 = DisableFC23,
            AutoProhibitReprobeInterval = AutoProhibitReprobeIntervalSeconds > 0
                ? TimeSpan.FromSeconds(AutoProhibitReprobeIntervalSeconds) : null,
            MaxReadGap = (ushort)Math.Clamp(MaxReadGap, 0, 65535),
            Family = Family,
            MelsecSubFamily = MelsecSubFamily,
            WriteOnChangeOnly = WriteOnChangeOnly,
            AutoReconnect = AutoReconnect,
            KeepAlive = new ModbusKeepAliveOptions
            {
                Enabled = KeepAliveEnabled,
                Time = TimeSpan.FromSeconds(KeepAliveTimeSeconds),
                Interval = TimeSpan.FromSeconds(KeepAliveIntervalSeconds),
                RetryCount = KeepAliveRetryCount,
            },
            IdleDisconnectTimeout = IdleDisconnectTimeoutSeconds > 0
                ? TimeSpan.FromSeconds(IdleDisconnectTimeoutSeconds) : null,
            Reconnect = new ModbusReconnectOptions
            {
                InitialDelay = TimeSpan.FromSeconds(ReconnectInitialDelaySeconds),
                MaxDelay = TimeSpan.FromSeconds(ReconnectMaxDelaySeconds),
                BackoffMultiplier = ReconnectBackoffMultiplier,
            },
            ProbeTimeoutSeconds = AdminProbeTimeoutSeconds,
        };
    }
}
