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

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

/// <summary>
///     AB CIP / EtherNet-IP driver configuration, bound from the driver's <c>DriverConfig</c>
///     JSON at <c>DriverHost.RegisterAsync</c>. One instance supports N devices (PLCs) behind
///     the same driver; per-device routing is keyed on the tag's device routing key (the RawPath's
///     device segment, delivered on <see cref="RawTagEntry.DeviceName"/>).
/// </summary>
public sealed class AbCipDriverOptions
{
    /// <summary>
    ///     PLCs this driver instance talks to. Each device contributes its own <see cref="AbCipHostAddress"/>
    ///     string as the <c>hostName</c> key used by resilience pipelines and the Admin UI.
    /// </summary>
    /// <remarks>
    ///     Wave C moves the device's live connection host into the <c>Device</c> row's <c>DeviceConfig</c>;
    ///     for Wave B the list stays bound from <c>DriverConfig</c> as today, and a tag routes to its device
    ///     by matching <see cref="RawTagEntry.DeviceName"/> against the device's name (<see cref="AbCipDeviceOptions.DeviceName"/>)
    ///     or its <see cref="AbCipDeviceOptions.HostAddress"/>.
    /// </remarks>
    public IReadOnlyList<AbCipDeviceOptions> Devices { get; init; } = [];

    /// <summary>
    ///     Authored raw tags this driver serves. The deploy artifact hands each authored raw
    ///     <c>Tag</c> as a <see cref="RawTagEntry"/> (RawPath identity + driver <c>TagConfig</c> blob +
    ///     WriteIdempotent flag + DeviceName routing key); the driver maps each through
    ///     <see cref="AbCipTagDefinitionFactory.FromTagConfig"/> into its RawPath → definition table.
    ///     Pre-declared tags always emit during discovery; opt in to controller-side discovery via
    ///     <see cref="EnableControllerBrowse"/>.
    /// </summary>
    public IReadOnlyList<RawTagEntry> RawTags { get; init; } = [];

    /// <summary>Per-device probe settings. Falls back to defaults when omitted.</summary>
    public AbCipProbeOptions Probe { get; init; } = new();

    /// <summary>
    ///     Default libplctag call timeout applied to reads/writes/discovery when the caller does
    ///     not pass a more specific value. Matches the Modbus driver's 2-second default.
    /// </summary>
    public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);

    /// <summary>
    ///     When <c>true</c>, <c>DiscoverAsync</c> walks each device's Logix symbol table via
    ///     the <c>@tags</c> pseudo-tag + surfaces controller-resident globals under a
    ///     <c>Discovered/</c> sub-folder. Pre-declared tags always emit regardless. Default
    ///     <c>false</c> to keep the strict-config path for deployments where only declared tags
    ///     should appear in the address space.
    /// </summary>
    public bool EnableControllerBrowse { get; init; }

    /// <summary>
    ///     When <c>true</c>, declared ALMD tags are surfaced as alarm conditions
    ///     via <c>IAlarmSource</c>; the driver polls each subscribed
    ///     alarm's <c>InFaulted</c> + <c>Severity</c> members + fires <c>OnAlarmEvent</c> on
    ///     state transitions. Default <c>false</c> — operators explicitly opt in because
    ///     projection semantics don't exactly mirror Rockwell FT Alarm &amp; Events; shops
    ///     running FT Live should keep this off + take alarms through the native route.
    /// </summary>
    public bool EnableAlarmProjection { get; init; }

    /// <summary>
    ///     Poll interval for the ALMD projection loop. Shorter intervals catch faster edges
    ///     at the cost of PLC round-trips; edges shorter than this interval are invisible to
    ///     the projection (a 0→1→0 transition within one tick collapses to no event). Default
    ///     1 second — matches typical SCADA alarm-refresh conventions.
    /// </summary>
    public TimeSpan AlarmPollInterval { get; init; } = TimeSpan.FromSeconds(1);

    /// <summary>
    ///     Opt-in for the declaration-only whole-UDT read fast path. When <c>false</c> (the
    ///     default) a batch of UDT members is always read per-member, because the byte offsets
    ///     computed by <see cref="AbCipUdtMemberLayout"/> assume the controller lays members
    ///     out in declaration order — and the Studio 5000 compiler does NOT guarantee that
    ///     (it reorders for largest-first packing, BOOL host bytes, nested-struct padding).
    ///     Decoding at declaration-order offsets against a reordered controller layout yields
    ///     silently-plausible wrong numbers. Set <c>true</c> only when the operator has
    ///     hand-verified that every configured UDT's member declaration order matches the
    ///     controller's compiled layout; in that case whole-UDT grouping collapses N member
    ///     reads into one. The richer CIP Template Object path remains the long-term fix.
    /// </summary>
    public bool EnableDeclarationOnlyUdtGrouping { get; init; }

    /// <summary>
    ///     Timeout for the AdminUI Test Connect probe, in seconds. The AdminUI clamps to a
    ///     60s server-side maximum; this default is what the form pre-fills for new instances.
    /// </summary>
    /// <remarks>Valid range: 1–60 seconds; the AdminUI clamps to 60s server-side.</remarks>
    public int ProbeTimeoutSeconds { get; init; } = 5;
}

/// <summary>
///     One PLC endpoint. <see cref="HostAddress"/> must parse via
///     <see cref="AbCipHostAddress.TryParse"/>; misconfigured devices fail driver
///     initialization rather than silently connecting to nothing.
/// </summary>
/// <param name="HostAddress">Canonical <c>ab://gateway[:port]/cip-path</c> string.</param>
/// <param name="PlcFamily">Which per-family profile to apply. Determines the family
///     <c>AllowPacking</c> default, <c>ConnectionSize</c> default, unconnected-only hint, and
///     other quirks; per-device overrides via <see cref="AllowPacking"/> and
///     <see cref="ConnectionSize"/> take precedence when set.</param>
/// <param name="DeviceName">Optional display label for Admin UI. Falls back to <see cref="HostAddress"/>.</param>
/// <param name="AllowPacking">Per-device override for CIP request-packing
///     (firmware 20+). <c>null</c> (the default) inherits the family profile's
///     <c>SupportsRequestPacking</c>; set explicitly to opt a single device in or out without
///     touching every other device on the same family.</param>
/// <param name="ConnectionSize">Per-device override for the Forward Open
///     ConnectionSize (Large Forward Open packet size in bytes). <c>null</c> inherits the family
///     profile's <c>DefaultConnectionSize</c>. Honoured by the driver layer; the underlying
///     libplctag 1.5.2 wrapper has no direct <c>ConnectionSize</c> property, so the value is
///     plumbed through <see cref="AbCipTagCreateParams"/> for forward-compat with future wrapper
///     versions or a custom tag-attribute path; current builds use the family profile default at
///     the wire layer regardless.</param>
public sealed record AbCipDeviceOptions(
    string HostAddress,
    AbCipPlcFamily PlcFamily = AbCipPlcFamily.ControlLogix,
    string? DeviceName = null,
    bool? AllowPacking = null,
    int? ConnectionSize = null);

/// <summary>
///     One AB-backed OPC UA variable. Mirrors the <c>ModbusTagDefinition</c> shape.
/// </summary>
/// <param name="Name">Tag name; becomes the OPC UA browse name and full reference (the RawPath in v3).</param>
/// <param name="DeviceHostAddress">The tag's <b>device routing key</b> — the RawPath's device segment,
///     threaded in from <see cref="RawTagEntry.DeviceName"/> by the driver at table-build time (the mapper
///     leaves it empty; device placement is no longer in the TagConfig blob). Resolved against the driver's
///     devices by matching <see cref="AbCipDeviceOptions.DeviceName"/> or <see cref="AbCipDeviceOptions.HostAddress"/>.
///     TODO(v3 WaveC): once the connection host moves to the Device row's DeviceConfig this is a pure logical
///     device name; the name still reads <c>DeviceHostAddress</c> to avoid churn across the driver + CLI.</param>
/// <param name="TagPath">Logix symbolic path (controller or program scope).</param>
/// <param name="DataType">Logix atomic type, or <see cref="AbCipDataType.Structure"/> for UDT-typed tags.</param>
/// <param name="Writable">When <c>true</c> and the tag's ExternalAccess permits writes, IWritable routes writes here.</param>
/// <param name="WriteIdempotent">Safe to replay on write timeout. Default <c>false</c>.</param>
/// <param name="Members">For <see cref="AbCipDataType.Structure"/>-typed tags, the declared UDT
///     member layout. When supplied, discovery fans out the UDT into a folder + one Variable per
///     member (member TagPath = <c>{tag.TagPath}.{member.Name}</c>). When <c>null</c> on a Structure
///     tag, the driver treats it as a black-box and relies on downstream configuration to address
///     members individually via dotted <see cref="AbCipTagPath"/> syntax. Ignored for atomic types.</param>
/// <param name="SafetyTag">GuardLogix safety-partition tag hint. When <c>true</c>, the driver
///     forces <c>SecurityClassification.ViewOnly</c> on discovery regardless of
///     <paramref name="Writable"/> — safety tags can only be written from the safety task of a
///     GuardLogix controller; non-safety writes violate the safety-partition isolation and are
///     rejected by the PLC anyway. Surfaces the intent explicitly instead of relying on the
///     write attempt failing at runtime.</param>
/// <param name="ElementCount">Phase 4c — number of array elements for a 1-D array tag. Reads via
///     libplctag's <c>elem_count</c> into an element-typed CLR array when <paramref name="IsArray"/>
///     is set; <c>1</c> for a scalar. Ignored for <see cref="AbCipDataType.Structure"/>.</param>
/// <param name="IsArray">Review I-1 — the EXPLICIT array signal. <c>true</c> ⟺ the source TagConfig
///     had <c>isArray:true</c> (with <c>arrayLength &gt;= 1</c>); the tag discovers as an OPC UA
///     array node (<c>IsArray</c> + <c>ArrayDim</c>) and reads as a typed CLR array — even when
///     <paramref name="ElementCount"/> is 1 (a valid 1-element array). <c>ElementCount</c> alone
///     cannot carry this because a scalar and a 1-element array both have a count of 1.
///     Ignored for <see cref="AbCipDataType.Structure"/>.</param>
public sealed record AbCipTagDefinition(
    string Name,
    string DeviceHostAddress,
    string TagPath,
    AbCipDataType DataType,
    bool Writable = true,
    bool WriteIdempotent = false,
    IReadOnlyList<AbCipStructureMember>? Members = null,
    bool SafetyTag = false,
    int ElementCount = 1,
    bool IsArray = false);

/// <summary>
///     One declared member of a UDT tag. Name is the member identifier on the PLC (e.g. <c>Speed</c>,
///     <c>Status</c>), DataType is the atomic Logix type, Writable/WriteIdempotent mirror
///     <see cref="AbCipTagDefinition"/>. Declaration-driven — the real CIP Template Object reader
///     (class 0x6C) that would auto-discover member layouts lands as a follow-up PR.
/// </summary>
/// <param name="Name">The member identifier on the PLC.</param>
/// <param name="DataType">The atomic Logix type of the member.</param>
/// <param name="Writable">Whether the member is writable.</param>
/// <param name="WriteIdempotent">Whether writes to the member are idempotent.</param>
/// <param name="ElementCount">Number of array elements for a 1-D array member; <c>1</c> for scalar.</param>
/// <param name="IsArray">Review I-1 — the EXPLICIT array signal for a member: <c>true</c> ⟺ the
///     member is a 1-D array (even of length 1). Discovers as an OPC UA array node when set.</param>
public sealed record AbCipStructureMember(
    string Name,
    AbCipDataType DataType,
    bool Writable = true,
    bool WriteIdempotent = false,
    int ElementCount = 1,
    bool IsArray = false);

/// <summary>Which AB PLC family the device is — selects the profile applied to connection params.</summary>
public enum AbCipPlcFamily
{
    ControlLogix,
    CompactLogix,
    Micro800,
    GuardLogix,
}

/// <summary>
///     Background connectivity-probe settings. Enabled by default; the probe reads a cheap tag
///     on the PLC at the configured interval to drive <c>IHostConnectivityProbe</c>
///     state transitions + Admin UI health status.
/// </summary>
public sealed class AbCipProbeOptions
{
    /// <summary>Gets a value indicating whether the probe is enabled.</summary>
    public bool Enabled { get; init; } = true;
    /// <summary>Gets the interval at which the probe reads the probe tag.</summary>
    public TimeSpan Interval { get; init; } = TimeSpan.FromSeconds(5);
    /// <summary>Gets the timeout for each probe read operation.</summary>
    public TimeSpan Timeout { get; init; } = TimeSpan.FromSeconds(2);

    /// <summary>
    ///     Tag path used for the probe. When <see cref="Enabled"/> is <c>true</c> but this is
    ///     <c>null</c>/blank, the driver logs a warning and runs no probe loops;
    ///     <c>GetHostStatuses()</c> will then report every device as <c>Unknown</c>. A family-default
    ///     system-tag fallback (e.g. <c>@raw_cpu_type</c> on ControlLogix) is a deferred follow-up;
    ///     today an operator opting into the probe must supply a tag path explicitly.
    /// </summary>
    public string? ProbeTagPath { get; init; }
}
