using S7.Net;
using S7NetDataType = global::S7.Net.DataType;

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

/// <summary>
///     Wire-layer abstraction over a single S7comm / ISO-on-TCP connection — the exact subset of
///     <see cref="Plc"/> members <see cref="S7Driver"/> uses. One instance per PLC, reused across
///     reads / writes / probes and serialized by the driver's single connection gate.
/// </summary>
/// <remarks>
///     The seam exists so <see cref="S7Driver.EnsureConnectedAsync"/> can dispose a dead handle and
///     re-open a fresh one after a transient PLC reboot / network blip — the reconnect path
///     — and so that path is unit-testable via a fake <see cref="IS7PlcFactory"/> without a
///     live PLC (S7.Net ships no in-process fake and <see cref="Plc"/> is <c>sealed</c>). Mirrors
///     the TwinCAT driver's <c>ITwinCATClient</c> / <c>ITwinCATClientFactory</c> pattern.
/// </remarks>
public interface IS7Plc : IDisposable
{
    /// <summary>True when the underlying TCP connection is established.</summary>
    bool IsConnected { get; }

    /// <summary>Opens the S7 connection (ISO-on-TCP handshake). Honours the ambient read/write timeouts baked in at construction.</summary>
    /// <param name="cancellationToken">Cancellation token for the connect attempt.</param>
    /// <returns>A task that completes when the connection is established.</returns>
    Task OpenAsync(CancellationToken cancellationToken);

    /// <summary>Closes the S7 connection. Best-effort and idempotent.</summary>
    void Close();

    /// <summary>Reads a scalar value by S7 address string (e.g. <c>DB1.DBW0</c>); the boxed type follows the size suffix.</summary>
    /// <param name="address">The S7 address string.</param>
    /// <param name="cancellationToken">Cancellation token for the read.</param>
    /// <returns>The boxed value, or <c>null</c> when the read produced no data.</returns>
    Task<object?> ReadAsync(string address, CancellationToken cancellationToken);

    /// <summary>Reads a contiguous byte block from an area — a single wire round-trip S7.Net chunks past the PDU limit.</summary>
    /// <param name="area">The S7 area (DataBlock / Memory / Input / Output / Timer / Counter).</param>
    /// <param name="db">Data-block number (0 for non-DB areas).</param>
    /// <param name="startByteAdr">Start byte offset (or timer/counter number for those areas).</param>
    /// <param name="count">Byte count to read.</param>
    /// <param name="cancellationToken">Cancellation token for the read.</param>
    /// <returns>The raw byte block.</returns>
    Task<byte[]> ReadBytesAsync(S7NetDataType area, int db, int startByteAdr, int count, CancellationToken cancellationToken);

    /// <summary>Writes a scalar value by S7 address string; the value must be the unsigned wire type for the address size.</summary>
    /// <param name="address">The S7 address string.</param>
    /// <param name="value">The wire-typed value to write.</param>
    /// <param name="cancellationToken">Cancellation token for the write.</param>
    /// <returns>A task that completes when the write is acknowledged.</returns>
    Task WriteAsync(string address, object value, CancellationToken cancellationToken);

    /// <summary>Writes a contiguous byte block to an area (buffer-based wide/structured write).</summary>
    /// <param name="area">The S7 area.</param>
    /// <param name="db">Data-block number (0 for non-DB areas).</param>
    /// <param name="startByteAdr">Start byte offset.</param>
    /// <param name="value">The byte block to write.</param>
    /// <param name="cancellationToken">Cancellation token for the write.</param>
    /// <returns>A task that completes when the write is acknowledged.</returns>
    Task WriteBytesAsync(S7NetDataType area, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken);

    /// <summary>Reads the CPU status (Run/Stop) — the light-weight probe PDU. The result is discarded; success means the PLC answered.</summary>
    /// <param name="cancellationToken">Cancellation token for the probe.</param>
    /// <returns>A task that completes when the CPU status PDU round-trips.</returns>
    Task ReadStatusAsync(CancellationToken cancellationToken);
}

/// <summary>Factory for <see cref="IS7Plc"/>s — one connection per PLC. Tests swap in a fake.</summary>
public interface IS7PlcFactory
{
    /// <summary>Creates a new, not-yet-opened <see cref="IS7Plc"/> for the given target.</summary>
    /// <param name="cpuType">The CPU family (drives the ISO-TSAP slot byte).</param>
    /// <param name="host">PLC IP address or hostname.</param>
    /// <param name="port">TCP port (102 on every S7 model).</param>
    /// <param name="rack">Hardware rack number.</param>
    /// <param name="slot">CPU slot.</param>
    /// <param name="timeout">Connect + per-operation timeout, applied to the underlying read/write timeouts.</param>
    /// <returns>A new, not-yet-opened <see cref="IS7Plc"/>.</returns>
    IS7Plc Create(S7CpuType cpuType, string host, int port, short rack, short slot, TimeSpan timeout);
}

/// <summary>
///     Default <see cref="IS7Plc"/> — a thin adapter over a real S7.Net <see cref="Plc"/>. Bakes the
///     read/write timeouts into the connection at construction so
///     <see cref="S7Driver.EnsureConnectedAsync"/>'s reopen reproduces the exact same configuration.
/// </summary>
internal sealed class S7PlcAdapter : IS7Plc
{
    private readonly Plc _plc;
    private readonly TimeSpan _operationTimeout;

    /// <summary>Initializes a new instance of the <see cref="S7PlcAdapter"/> class.</summary>
    /// <param name="cpuType">The CPU family.</param>
    /// <param name="host">PLC IP address or hostname.</param>
    /// <param name="port">TCP port.</param>
    /// <param name="rack">Hardware rack number.</param>
    /// <param name="slot">CPU slot.</param>
    /// <param name="timeout">Connect + per-operation timeout.</param>
    public S7PlcAdapter(S7CpuType cpuType, string host, int port, short rack, short slot, TimeSpan timeout)
    {
        _plc = new Plc(S7CpuTypeMap.ToS7Net(cpuType), host, port, rack, slot);
        _operationTimeout = timeout;
        // S7netplus writes timeouts into the underlying TcpClient via ReadTimeout / WriteTimeout
        // (milliseconds). Set before OpenAsync so the handshake itself honours the bound — matches
        // the original inline construction in S7Driver.InitializeAsync. NOTE: these map to the
        // TcpClient's ReceiveTimeout / SendTimeout, which govern only SYNCHRONOUS socket calls; the
        // async read/write paths S7.Net actually uses ignore them, so every data-plane op below is
        // additionally bounded by S7OperationDeadline (a real wall-clock ceiling). OpenAsync is left
        // to S7Driver.EnsureConnectedAsync's own CancelAfter, so it is not double-bounded here.
        var ms = (int)timeout.TotalMilliseconds;
        _plc.ReadTimeout = ms;
        _plc.WriteTimeout = ms;
    }

    /// <inheritdoc />
    public bool IsConnected => _plc.IsConnected;

    /// <inheritdoc />
    public Task OpenAsync(CancellationToken cancellationToken) => _plc.OpenAsync(cancellationToken);

    /// <inheritdoc />
    public void Close() => _plc.Close();

    /// <inheritdoc />
    public Task<object?> ReadAsync(string address, CancellationToken cancellationToken) =>
        S7OperationDeadline.RunAsync(
            t => _plc.ReadAsync(address, t)!, _operationTimeout, $"read of '{address}'", cancellationToken);

    /// <inheritdoc />
    public Task<byte[]> ReadBytesAsync(S7NetDataType area, int db, int startByteAdr, int count, CancellationToken cancellationToken) =>
        S7OperationDeadline.RunAsync(
            t => _plc.ReadBytesAsync(area, db, startByteAdr, count, t),
            _operationTimeout, $"block read of {area} db{db} @{startByteAdr}+{count}", cancellationToken);

    /// <inheritdoc />
    public Task WriteAsync(string address, object value, CancellationToken cancellationToken) =>
        S7OperationDeadline.RunAsync(
            t => _plc.WriteAsync(address, value, t), _operationTimeout, $"write of '{address}'", cancellationToken);

    /// <inheritdoc />
    public Task WriteBytesAsync(S7NetDataType area, int db, int startByteAdr, byte[] value, CancellationToken cancellationToken) =>
        S7OperationDeadline.RunAsync(
            t => _plc.WriteBytesAsync(area, db, startByteAdr, value, t),
            _operationTimeout, $"block write of {area} db{db} @{startByteAdr}", cancellationToken);

    /// <inheritdoc />
    public Task ReadStatusAsync(CancellationToken cancellationToken) =>
        S7OperationDeadline.RunAsync(
            t => _plc.ReadStatusAsync(t), _operationTimeout, "status read", cancellationToken);

    /// <inheritdoc />
    // Plc implements IDisposable explicitly, so cast to invoke it; disposal tears down the TcpClient.
    public void Dispose() => ((IDisposable)_plc).Dispose();
}

/// <summary>Default <see cref="IS7PlcFactory"/> producing real S7.Net-backed <see cref="S7PlcAdapter"/>s.</summary>
internal sealed class S7PlcFactory : IS7PlcFactory
{
    /// <inheritdoc />
    public IS7Plc Create(S7CpuType cpuType, string host, int port, short rack, short slot, TimeSpan timeout) =>
        new S7PlcAdapter(cpuType, host, port, rack, slot, timeout);
}
