using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.Driver.Modbus;

/// <summary>
///     Modbus TCP driver configuration. Bound from the driver's <c>DriverConfig</c> JSON at
///     <c>DriverHost.RegisterAsync</c>. Every register the driver exposes appears in
///     <see cref="Tags"/>; names become the OPC UA browse name + full reference.
/// </summary>
public sealed class ModbusDriverOptions
{
    public string Host { get; init; } = "127.0.0.1";
    public int Port { get; init; } = 502;
    public byte UnitId { get; init; } = 1;
    public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);

    /// <summary>Pre-declared tag map. Modbus has no discovery protocol — the driver returns exactly these.</summary>
    public IReadOnlyList<ModbusTagDefinition> Tags { get; init; } = [];

    /// <summary>
    ///     Background connectivity-probe settings. When <see cref="ModbusProbeOptions.Enabled"/>
    ///     is true the driver runs a tick loop that issues a cheap FC03 at register 0 every
    ///     <see cref="ModbusProbeOptions.Interval"/> and raises <c>OnHostStatusChanged</c> on
    ///     Running ↔ Stopped transitions. The Admin UI / OPC UA clients see the state through
    ///     <see cref="IHostConnectivityProbe"/>.
    /// </summary>
    public ModbusProbeOptions Probe { get; init; } = new();

    /// <summary>
    ///     Maximum registers per FC03 (Read Holding Registers) / FC04 (Read Input Registers)
    ///     transaction. Modbus-TCP spec allows 125; many device families impose lower caps:
    ///     AutomationDirect DL205/DL260 cap at <c>128</c>, Mitsubishi Q/FX3U cap at <c>64</c>,
    ///     Omron CJ/CS cap at <c>125</c>. Set to the lowest cap across the devices this driver
    ///     instance talks to; the driver auto-chunks larger reads into consecutive requests.
    ///     Default <c>125</c> — the spec maximum, safe against any conforming server. Setting
    ///     to <c>0</c> disables the cap (discouraged — the spec upper bound still applies).
    /// </summary>
    public ushort MaxRegistersPerRead { get; init; } = 125;

    /// <summary>
    ///     Maximum registers per FC16 (Write Multiple Registers) transaction. Spec maximum is
    ///     <c>123</c>; DL205/DL260 cap at <c>100</c>. Matching caller-vs-device semantics:
    ///     exceeding the cap currently throws (writes aren't auto-chunked because a partial
    ///     write across two FC16 calls is no longer atomic — caller must explicitly opt in
    ///     by shortening the tag's <c>StringLength</c> or splitting it into multiple tags).
    /// </summary>
    public ushort MaxRegistersPerWrite { get; init; } = 123;

    /// <summary>
    ///     When <c>true</c> (default) the built-in <see cref="ModbusTcpTransport"/> detects
    ///     mid-transaction socket failures (<see cref="System.IO.EndOfStreamException"/>,
    ///     <see cref="System.Net.Sockets.SocketException"/>) and transparently reconnects +
    ///     retries the PDU exactly once. Required for DL205/DL260 because the H2-ECOM100
    ///     does not send TCP keepalives — intermediate NAT / firewall devices silently close
    ///     idle sockets and the first send after the drop would otherwise surface as a
    ///     connection error to the caller even though the PLC is up.
    /// </summary>
    public bool AutoReconnect { get; init; } = true;
}

public sealed class ModbusProbeOptions
{
    public bool Enabled { get; init; } = true;
    public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
    public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);
    /// <summary>Register to read for the probe. Zero is usually safe; override for PLCs that lock register 0.</summary>
    public ushort ProbeAddress { get; init; } = 0;
}

/// <summary>
///     One Modbus-backed OPC UA variable. Address is zero-based (Modbus spec numbering, not
///     the documentation's 1-based coil/register conventions). Multi-register types
///     (Int32/UInt32/Float32 = 2 regs; Int64/UInt64/Float64 = 4 regs) respect the
///     <see cref="ByteOrder"/> field — real-world PLCs disagree on word ordering.
/// </summary>
/// <param name="Name">
///     Tag name, used for both the OPC UA browse name and the driver's full reference. Must be
///     unique within the driver.
/// </param>
/// <param name="Region">Coils / DiscreteInputs / InputRegisters / HoldingRegisters.</param>
/// <param name="Address">Zero-based address within the region.</param>
/// <param name="DataType">
///     Logical data type. See <see cref="ModbusDataType"/> for the register count each encodes.
/// </param>
/// <param name="Writable">When true and Region supports writes (Coils / HoldingRegisters), IWritable routes writes here.</param>
/// <param name="ByteOrder">Word ordering for multi-register types. Ignored for Bool / Int16 / UInt16 / BitInRegister / String.</param>
/// <param name="BitIndex">For <c>DataType = BitInRegister</c>: which bit of the holding register (0-15, LSB-first).</param>
/// <param name="StringLength">For <c>DataType = String</c>: number of ASCII characters (2 per register, rounded up).</param>
/// <param name="StringByteOrder">
///     Per-register byte order for <c>DataType = String</c>. Standard Modbus packs the first
///     character in the high byte (<see cref="ModbusStringByteOrder.HighByteFirst"/>).
///     AutomationDirect DirectLOGIC (DL205/DL260) and a few legacy families pack the first
///     character in the low byte instead — see <c>docs/v2/dl205.md</c> §strings.
/// </param>
/// <param name="WriteIdempotent">
///     Per <c>docs/v2/plan.md</c> decisions #44, #45, #143 — flag a tag as safe to replay on
///     write timeout / failure. Default <c>false</c>; writes do not auto-retry. Safe candidates:
///     holding-register set-points for analog values and configuration registers where the same
///     value can be written again without side-effects. Unsafe: coils that drive edge-triggered
///     actions (pulse outputs), counter-increment addresses on PLCs that treat writes as deltas,
///     any BCD / counter register where repeat-writes advance state.
/// </param>
public sealed record ModbusTagDefinition(
    string Name,
    ModbusRegion Region,
    ushort Address,
    ModbusDataType DataType,
    bool Writable = true,
    ModbusByteOrder ByteOrder = ModbusByteOrder.BigEndian,
    byte BitIndex = 0,
    ushort StringLength = 0,
    ModbusStringByteOrder StringByteOrder = ModbusStringByteOrder.HighByteFirst,
    bool WriteIdempotent = false);

public enum ModbusRegion { Coils, DiscreteInputs, InputRegisters, HoldingRegisters }

public enum ModbusDataType
{
    Bool,
    Int16,
    UInt16,
    Int32,
    UInt32,
    Int64,
    UInt64,
    Float32,
    Float64,
    /// <summary>Single bit within a holding register. <see cref="ModbusTagDefinition.BitIndex"/> selects 0-15 LSB-first.</summary>
    BitInRegister,
    /// <summary>ASCII string packed 2 chars per register, <see cref="ModbusTagDefinition.StringLength"/> characters long.</summary>
    String,
    /// <summary>
    ///     16-bit binary-coded decimal. Each nibble encodes one decimal digit (0-9). Register
    ///     value <c>0x1234</c> decodes as decimal <c>1234</c> — NOT binary <c>0x04D2 = 4660</c>.
    ///     DL205/DL260 and several Mitsubishi / Omron families store timers, counters, and
    ///     operator-facing numerics as BCD by default.
    /// </summary>
    Bcd16,
    /// <summary>
    ///     32-bit (two-register) BCD. Decodes 8 decimal digits. Word ordering follows
    ///     <see cref="ModbusTagDefinition.ByteOrder"/> the same way <see cref="Int32"/> does.
    /// </summary>
    Bcd32,
}

/// <summary>
///     Word ordering for multi-register types. Modbus TCP standard is <see cref="BigEndian"/>
///     (ABCD for 32-bit: high word at the lower address). Many PLCs — Siemens S7, several
///     Allen-Bradley series, some Modicon families — use <see cref="WordSwap"/> (CDAB), which
///     keeps bytes big-endian within each register but reverses the word pair(s).
/// </summary>
public enum ModbusByteOrder
{
    BigEndian,
    WordSwap,
}

/// <summary>
///     Per-register byte order for ASCII strings packed 2 chars per register. Standard Modbus
///     convention is <see cref="HighByteFirst"/> — the first character of each pair occupies
///     the high byte of the register. AutomationDirect DirectLOGIC (DL205, DL260, DL350) and a
///     handful of legacy controllers pack <see cref="LowByteFirst"/>, which inverts that within
///     each register. Word ordering across multiple registers is always ascending address for
///     strings — only the byte order inside each register flips.
/// </summary>
public enum ModbusStringByteOrder
{
    HighByteFirst,
    LowByteFirst,
}
