using CliFx.Attributes;
using CliFx.Infrastructure;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Cli.Common;

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

/// <summary>
///     Long-running poll of one Modbus register via the driver's <c>ISubscribable</c> surface
///     (under the hood: <c>PollGroupEngine</c>). Prints each data-change event until the
///     operator Ctrl+C's the CLI. Useful for watching a changing PLC signal during
///     commissioning or while reproducing a customer bug.
/// </summary>
[Command("subscribe", Description = "Watch a Modbus register via polled subscription until Ctrl+C.")]
public sealed class SubscribeCommand : ModbusCommandBase
{
    /// <summary>Gets or sets the Modbus register region (Coils, DiscreteInputs, InputRegisters, or HoldingRegisters).</summary>
    [CommandOption("region", 'r', Description =
        "Coils / DiscreteInputs / InputRegisters / HoldingRegisters", IsRequired = true)]
    public ModbusRegion Region { get; init; }

    /// <summary>Gets or sets the zero-based address within the region.</summary>
    [CommandOption("address", 'a', Description = "Zero-based address within the region.", IsRequired = true)]
    public ushort Address { get; init; }

    /// <summary>Gets or sets the data type (Bool, Int16, UInt16, Int32, UInt32, Int64, UInt64, Float32, Float64, BitInRegister, String, Bcd16, Bcd32).</summary>
    [CommandOption("type", 't', Description =
        "Bool / Int16 / UInt16 / Int32 / UInt32 / Int64 / UInt64 / Float32 / Float64 / " +
        "BitInRegister / String / Bcd16 / Bcd32", IsRequired = true)]
    public ModbusDataType DataType { get; init; }

    /// <summary>Gets or sets the publishing interval in milliseconds.</summary>
    [CommandOption("interval-ms", 'i', Description =
        "Publishing interval in milliseconds (default 1000). The PollGroupEngine enforces " +
        "a floor of ~250ms; values below it get rounded up.")]
    public int IntervalMs { get; init; } = 1000;

    /// <summary>Gets or sets the byte order (BigEndian or WordSwap).</summary>
    [CommandOption("byte-order", Description =
        "BigEndian (default) or WordSwap.")]
    public ModbusByteOrder ByteOrder { get; init; } = ModbusByteOrder.BigEndian;

    // subscribe previously lacked these three options that read and
    // write both expose. Without them, BitInRegister always watches bit 0 and String runs with
    // StringLength=0, silently producing wrong results for any subscriber using those types.
    /// <summary>Gets or sets the bit index for type=BitInRegister (0-15, LSB-first).</summary>
    [CommandOption("bit-index", Description =
        "For type=BitInRegister: which bit of the holding register (0-15, LSB-first).")]
    public byte BitIndex { get; init; }

    /// <summary>Gets or sets the string length for type=String (character count, 2 per register).</summary>
    [CommandOption("string-length", Description =
        "For type=String: character count (2 per register, rounded up).")]
    public ushort StringLength { get; init; }

    /// <summary>Gets or sets the string byte order for type=String (HighByteFirst or LowByteFirst).</summary>
    [CommandOption("string-byte-order", Description =
        "For type=String: HighByteFirst (standard) or LowByteFirst (DirectLOGIC).")]
    public ModbusStringByteOrder StringByteOrder { get; init; } = ModbusStringByteOrder.HighByteFirst;

    /// <inheritdoc />
    public override async ValueTask ExecuteAsync(IConsole console)
    {
        ConfigureLogging();
        ValidateEndpoint();
        // Reject non-positive interval before opening the driver so
        // the operator gets a clean error instead of a confusing PollGroupEngine failure.
        ValidateInterval(IntervalMs);
        var ct = console.RegisterCancellationHandler();

        var tagName = ReadCommand.SynthesiseTagName(Region, Address, DataType);
        var tag = new ModbusTagDefinition(
            Name: tagName,
            Region: Region,
            Address: Address,
            DataType: DataType,
            Writable: false,
            ByteOrder: ByteOrder,
            BitIndex: BitIndex,
            StringLength: StringLength,
            StringByteOrder: StringByteOrder);
        var options = BuildOptions([tag]);

        await using var driver = new ModbusDriver(options, DriverInstanceId);
        // Serialize console writes from the PollGroupEngine background
        // thread so overlapping poll ticks can't interleave partial lines.
        var writeLock = new object();
        ISubscriptionHandle? handle = null;
        try
        {
            await driver.InitializeAsync("{}", ct);

            // Emit the banner BEFORE wiring OnDataChange so the
            // main-thread WriteLineAsync cannot interleave with poll-thread change-event
            // writes. TextWriter.WriteLine is not guaranteed thread-safe; once the handler
            // is attached and SubscribeAsync starts, change events run on the poll thread.
            // Mirrors the ordering in Driver.AbCip.Cli.SubscribeCommand.
            await console.Output.WriteLineAsync(
                $"Subscribed to {tagName} @ {IntervalMs}ms. Ctrl+C to stop.");

            // Route every data-change event to the CliFx console (not System.Console — the
            // analyzer flags it + IConsole is the testable abstraction).
            driver.OnDataChange += (_, e) =>
            {
                // Swallow + log write failures so a transient stdout
                // error (closed pipe, IO exception on a redirected stream) cannot tear down
                // the poll-engine background loop. Without this guard the unhandled
                // exception would fault the long-running subscribe.
                try
                {
                    var line = $"[{DateTime.UtcNow:HH:mm:ss.fff}] " +
                        $"{e.FullReference} = {SnapshotFormatter.FormatValue(e.Snapshot.Value)} " +
                        $"({SnapshotFormatter.FormatStatus(e.Snapshot.StatusCode)})";
                    lock (writeLock)
                    {
                        console.Output.WriteLine(line);
                    }
                }
                catch (Exception ex)
                {
                    Serilog.Log.Logger.Warning(ex,
                        "SubscribeCommand: console write failed for {Tag}; continuing poll loop.",
                        e.FullReference);
                }
            };

            handle = await driver.SubscribeAsync([tagName], TimeSpan.FromMilliseconds(IntervalMs), ct);
            try
            {
                await Task.Delay(System.Threading.Timeout.InfiniteTimeSpan, ct);
            }
            catch (OperationCanceledException)
            {
                // Expected on Ctrl+C — fall through to the unsubscribe in finally.
            }
        }
        finally
        {
            if (handle is not null)
            {
                try { await driver.UnsubscribeAsync(handle, CancellationToken.None); }
                catch { /* teardown best-effort */ }
            }
            await driver.ShutdownAsync(CancellationToken.None);
            // Flush Serilog before process exit so buffered log lines
            // emitted just before Ctrl+C (e.g. during UnsubscribeAsync / ShutdownAsync) are
            // not lost. Matches the pattern in Driver.AbCip.Cli commands.
            FlushLogging();
        }
    }

    /// <summary>
    ///     Guards <c>--interval-ms</c> against zero or negative values.
    ///     A non-positive interval would produce a non-positive <see cref="TimeSpan"/> into
    ///     <c>SubscribeAsync</c>; the CLI should fail fast with an actionable error rather
    ///     than relying on the downstream <c>PollGroupEngine</c> to clamp the value.
    ///     Mirrors <c>Driver.AbCip.Cli.SubscribeCommand.ValidateInterval</c>.
    /// </summary>
    /// <param name="intervalMs">The polling interval in milliseconds.</param>
    internal static void ValidateInterval(int intervalMs)
    {
        if (intervalMs <= 0)
            throw new CliFx.Exceptions.CommandException(
                $"--interval-ms must be > 0 (got {intervalMs}). " +
                "PollGroupEngine floors sub-250ms values, but accepts any positive integer.");
    }
}
