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>
///     Probes a Modbus-TCP endpoint: opens a socket via <see cref="ModbusDriver"/>'s
///     <c>InitializeAsync</c>, issues a single FC03 at the configured probe address, and
///     prints the driver's <c>GetHealth()</c>. Fastest way to answer "is the PLC up + talking
///     Modbus on this host:port?".
/// </summary>
[Command("probe", Description = "Verify the Modbus-TCP endpoint is reachable and speaks Modbus.")]
public sealed class ProbeCommand : ModbusCommandBase
{
    /// <summary>Gets the holding-register address to use for the probe read.</summary>
    [CommandOption("probe-address", Description =
        "Holding-register address used as the cheap-read probe (default 0). Some PLCs lock " +
        "register 0 — set this to a known-good address on your device.")]
    public ushort ProbeAddress { get; init; }

    /// <inheritdoc />
    public override async ValueTask ExecuteAsync(IConsole console)
    {
        ConfigureLogging();
        ValidateEndpoint();
        var ct = console.RegisterCancellationHandler();

        // Build with one probe tag + Probe.Enabled=false so InitializeAsync connects the
        // transport, we issue a single read to verify the device responds, then shut down.
        var probeTag = new ModbusTagDefinition(
            Name: "__probe",
            Region: ModbusRegion.HoldingRegisters,
            Address: ProbeAddress,
            DataType: ModbusDataType.UInt16);
        var options = BuildOptions([probeTag]);

        await using var driver = new ModbusDriver(options, DriverInstanceId);
        try
        {
            await driver.InitializeAsync("{}", ct);
            var snapshot = await driver.ReadAsync(["__probe"], ct);
            var health = driver.GetHealth();

            // Derive the headline verdict from BOTH the driver state
            // AND the probe-read StatusCode so the operator never sees the previous
            // contradictory pair (`Health: Healthy` over a Bad snapshot line). The bare
            // driver state is still printed below for diagnostics, but the verdict is what
            // the operator scans first.
            var verdict = ComputeVerdict(health.State, snapshot[0].StatusCode);

            await console.Output.WriteLineAsync($"Host:         {Host}:{Port} (unit {UnitId})");
            await console.Output.WriteLineAsync($"Verdict:      {verdict}");
            await console.Output.WriteLineAsync($"Driver state: {health.State}");
            if (health.LastError is { } err)
                await console.Output.WriteLineAsync($"Last error:   {err}");
            await console.Output.WriteLineAsync();
            await console.Output.WriteLineAsync(
                SnapshotFormatter.Format($"HR[{ProbeAddress}]", snapshot[0]));
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            // Ctrl+C during InitializeAsync — exit quietly so CliFx
            // does not render a full stack trace for a user-initiated cancellation.
            await console.Output.WriteLineAsync("Cancelled.");
        }
        finally
        {
            await driver.ShutdownAsync(CancellationToken.None);
            // Flush Serilog before process exit so buffered log lines
            // emitted during driver shutdown are not lost. Matches Driver.AbCip.Cli pattern.
            FlushLogging();
        }
    }

    /// <summary>
    ///     Combine the driver-side <see cref="DriverState"/> with the
    ///     probe snapshot's OPC UA <c>StatusCode</c> into a single headline verdict. Order
    ///     of precedence:
    ///     <list type="number">
    ///       <item><c>FAIL</c> — driver did not reach <see cref="DriverState.Healthy"/>
    ///         (Faulted / Reconnecting / Unknown) OR the snapshot reports Bad quality.</item>
    ///       <item><c>DEGRADED</c> — driver Healthy but the snapshot quality is Uncertain.</item>
    ///       <item><c>OK</c> — driver Healthy and snapshot Good.</item>
    ///     </list>
    /// </summary>
    /// <param name="state">The driver's health state.</param>
    /// <param name="statusCode">The OPC UA status code from the probe read.</param>
    /// <returns>A string describing the verdict.</returns>
    public static string ComputeVerdict(DriverState state, uint statusCode)
    {
        // OPC UA StatusCode top 2 bits encode the quality class:
        //   0x00xxxxxx → Good, 0x40xxxxxx → Uncertain, 0x80xxxxxx / 0xC0xxxxxx → Bad.
        var qualityClass = statusCode & 0xC0000000u;
        var snapshotGood = qualityClass == 0x00000000u;
        var snapshotUncertain = qualityClass == 0x40000000u;

        if (state != DriverState.Healthy || !snapshotGood && !snapshotUncertain)
            return $"FAIL (driver={state}, probe={SnapshotFormatter.FormatStatus(statusCode)})";

        if (snapshotUncertain)
            return $"DEGRADED (driver={state}, probe={SnapshotFormatter.FormatStatus(statusCode)})";

        return $"OK (driver={state}, probe={SnapshotFormatter.FormatStatus(statusCode)})";
    }
}
