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

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

/// <summary>
///     Probes an S7 endpoint: connects via S7.Net, reads one merker word, prints health.
///     If the PLC is fresh out of TIA Portal the probe will surface
///     <c>BadNotSupported</c> — PUT/GET communication has to be enabled in the hardware
///     config for any S7-1200/1500 for the driver to get past the handshake.
/// </summary>
[Command("probe", Description = "Verify the S7 endpoint is reachable and a sample read succeeds.")]
public sealed class ProbeCommand : S7CommandBase
{
    /// <summary>Gets or sets the S7 address to probe.</summary>
    [CommandOption("address", 'a', Description =
        "Probe address (default MW0 — merker word 0). DB1.DBW0 if your PLC project " +
        "reserves a fingerprint DB.")]
    public string Address { get; init; } = "MW0";

    /// <summary>Gets or sets the data type of the probe address.</summary>
    [CommandOption("type", Description = "Probe data type (default Int16).")]
    public S7DataType DataType { get; init; } = S7DataType.Int16;

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

        var probeTag = new S7TagDefinition(
            Name: "__probe",
            Address: Address,
            DataType: DataType,
            Writable: false);
        var options = BuildOptions([probeTag]);

        // `await using` is the sole disposal mechanism — S7Driver.DisposeAsync
        // already invokes ShutdownAsync, so the previous explicit ShutdownAsync(CancellationToken.None)
        // call in a finally block ran shutdown twice. The await-using on the next line is enough.
        await using var driver = new S7Driver(options, DriverInstanceId);
        // Wrap the entire probe sequence so that a refused/unreachable TCP
        // connect still prints the structured Host/CPU/Health lines instead of crashing with a
        // full .NET stack trace. InitializeAsync sets health to Faulted with the exception
        // message before re-throwing, so GetHealth() always has something to report.
        try
        {
            await driver.InitializeAsync("{}", ct);
            var snapshot = await driver.ReadAsync(["__probe"], ct);
            var health = driver.GetHealth();

            await console.Output.WriteLineAsync($"Host:         {Host}:{Port}");
            await console.Output.WriteLineAsync($"CPU:          {CpuType} rack={Rack} slot={Slot}");
            await console.Output.WriteLineAsync($"Health:       {health.State}");
            if (health.LastError is { } err)
                await console.Output.WriteLineAsync($"Last error:   {err}");
            await console.Output.WriteLineAsync();
            await console.Output.WriteLineAsync(SnapshotFormatter.Format(Address, snapshot[0]));
        }
        catch (OperationCanceledException) when (ct.IsCancellationRequested)
        {
            // Ctrl+C — re-throw so CliFx exits cleanly. The when filter ensures a
            // driver-internal timeout (different CancellationToken) is not mis-classified
            // as user cancellation and falls through to the structured-report catch below.
            throw;
        }
        catch
        {
            // Connect / read failure — print what the driver knows so far.
            var health = driver.GetHealth();
            await console.Output.WriteLineAsync($"Host:         {Host}:{Port}");
            await console.Output.WriteLineAsync($"CPU:          {CpuType} rack={Rack} slot={Slot}");
            await console.Output.WriteLineAsync($"Health:       {health.State}");
            if (health.LastError is { } err)
                await console.Output.WriteLineAsync($"Last error:   {err}");
        }
    }
}
