using Microsoft.Extensions.Logging;
using ZB.MOM.WW.MxGateway.Client;
using ZB.MOM.WW.MxGateway.Contracts.Proto;

namespace ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Runtime;

/// <summary>
///     Maps the gateway's <see cref="MxStatusProxy"/> (raw MXAccess HRESULT + category bits)
///     to OPC UA <c>StatusCode</c> uints. Replaces the legacy
///     <c>MxAccessGalaxyBackend.ToWire</c> heuristic (Quality &gt;= 192 → Good, else Uncertain)
///     with an explicit table that preserves specific codes (BadNotConnected, OutOfService,
///     UncertainSubNormal, etc.) instead of collapsing to category buckets.
/// </summary>
/// <remarks>
///     OPC DA quality bytes are 16-bit values arranged as <c>[QQSSSSSSLLNNNN]</c>:
///     Q = quality category (Bad/Uncertain/Good = 0/1/3), S = substatus, L = limit, N = vendor.
///     This mapper consumes the LOW byte (where the Q+S bits live) — the same byte the legacy
///     Wonderware Historian SDK exposed as the raw quality byte. Category-only fallback paths
///     handle deployment versions of MXAccess that surface unfamiliar substatuses.
///
///     Unknown substatus values fall back to the matching category bucket (<c>Good</c>,
///     <c>Uncertain</c>, <c>Bad</c>) and emit a single diagnostic log line per session via
///     the supplied logger so field captures can extend the table.
/// </remarks>
internal static class StatusCodeMap
{
    // OPC UA Part 4 standard StatusCodes — top-byte categories are 0x00 (Good),
    // 0x40 (Uncertain), 0x80 (Bad). Specific codes layer onto the category byte.
    //
    // The substatus nibbles are NOT derivable from the OPC DA quality byte this mapper consumes —
    // they are an unrelated OPC UA enumeration and have to be looked up. Five constants here were
    // originally written as though the DA byte could be shifted into the substatus position, which
    // produced values naming entirely different UA codes (Gitea #497): UncertainLastUsableValue held
    // UncertainDataSubNormal's value, UncertainSubNormal held UncertainNoCommunicationLastUsableValue's,
    // and GoodLocalOverride / UncertainSensorNotAccurate / UncertainEngineeringUnitsExceeded held values
    // that are not OPC UA status codes at all. StatusCodeParityTests now checks every constant below
    // against the pinned SDK's Opc.Ua.StatusCodes.

    public const uint Good                            = 0x00000000u;
    public const uint GoodLocalOverride               = 0x00960000u;
    public const uint Uncertain                       = 0x40000000u;
    public const uint UncertainLastUsableValue        = 0x40900000u;
    public const uint UncertainSensorNotAccurate      = 0x40930000u;
    public const uint UncertainEngineeringUnitsExceeded = 0x40940000u;
    public const uint UncertainSubNormal              = 0x40950000u;
    public const uint Bad                             = 0x80000000u;
    public const uint BadConfigurationError           = 0x80890000u;
    public const uint BadNotConnected                 = 0x808A0000u;
    public const uint BadDeviceFailure                = 0x808B0000u;
    public const uint BadSensorFailure                = 0x808C0000u;
    public const uint BadCommunicationError           = 0x80050000u;
    public const uint BadOutOfService                 = 0x808D0000u;
    public const uint BadWaitingForInitialData        = 0x80320000u;
    public const uint BadInternalError                = 0x80020000u;

    /// <summary>
    ///     Fills a still-pending read when the caller's token fires before the gateway answers. Named here
    ///     rather than written inline at the call site so <c>StatusCodeParityTests</c> can reflect over it —
    ///     an inline literal is invisible to that guard, which is exactly how this constant spent its life
    ///     as <c>0x800B0000</c> (<c>BadServiceUnsupported</c>) under a <c>// BadTimeout</c> comment.
    /// </summary>
    public const uint BadTimeout                      = 0x800A0000u;

    /// <summary>
    ///     Map a raw OPC DA quality byte (the low byte of an OPC DA <c>OpcQuality</c> ushort,
    ///     which is what Wonderware Historian + MXAccess surface as <c>OPCITEMSTATE.qLong</c>'s
    ///     low byte) to the OPC UA StatusCode uint.
    /// </summary>
    /// <param name="q">The OPC DA quality byte to convert.</param>
    /// <param name="logger">Optional logger for diagnostics on unknown quality bytes.</param>
    /// <returns>The OPC UA status code.</returns>
    public static uint FromQualityByte(byte q, ILogger? logger = null) => q switch
    {
        // Good family — top two bits 11b (192-255).
        192 => Good,
        216 => GoodLocalOverride,

        // Uncertain family — top two bits 01b (64-127).
        64  => Uncertain,
        68  => UncertainLastUsableValue,
        80  => UncertainSensorNotAccurate,
        84  => UncertainEngineeringUnitsExceeded,
        88  => UncertainSubNormal,

        // Bad family — top two bits 00b (0-63).
        0   => Bad,
        4   => BadConfigurationError,
        8   => BadNotConnected,
        12  => BadDeviceFailure,
        16  => BadSensorFailure,
        20  => BadCommunicationError,
        24  => BadOutOfService,
        32  => BadWaitingForInitialData,

        _ => Categorize(q, logger),
    };

    /// <summary>
    ///     Map a gateway-reported <see cref="MxStatusProxy"/> to OPC UA StatusCode. Uses
    ///     <see cref="MxStatusProxyExtensions.IsSuccess"/> (category == OK AND success != 0)
    ///     as the canonical success test — the proto contract explicitly documents that
    ///     <c>success</c> is NOT a boolean and must not be checked in isolation; category is
    ///     the authoritative indicator. On failure, the detail byte (OPC DA quality substatus)
    ///     drives the specific code, with a transport-error fallback for pre-MXAccess failures.
    /// </summary>
    /// <param name="status">The MX status proxy to convert, or null for Good status.</param>
    /// <param name="logger">Optional logger for diagnostics on unknown status codes.</param>
    /// <returns>The OPC UA status code.</returns>
    public static uint FromMxStatus(MxStatusProxy? status, ILogger? logger = null)
    {
        if (status is null) return Good;
        if (status.IsSuccess()) return Good;

        // Detail field carries the substatus when the worker translated MX-style codes;
        // when zero, infer from detected_by.
        var detail = (byte)(status.Detail & 0xFF);
        if (detail != 0) return FromQualityByte(detail, logger);

        // detected_by != Mxaccess (raw_detected_by != the MXAccess source enum) implies
        // the failure happened pre-call (gateway, worker, transport) — surface as a
        // communication error rather than a generic Bad.
        if (status.RawDetectedBy != 0) return BadCommunicationError;

        return Bad;
    }

    /// <summary>
    ///     Convert an OPC UA status-code uint back to the OPC DA quality category
    ///     byte — Good=192, Uncertain=64, Bad=0 — by extracting the top-two bits of the
    ///     high word. This is the inverse of the category-bucket arm of
    ///     <see cref="FromQualityByte"/>. It is intentionally lossy (substatus bits are not
    ///     round-tripped) because the sole consumer
    ///     (<see cref="ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Health.PerPlatformProbeWatcher"/>)
    ///     only tests <c>qualityByte &lt; 192</c> to distinguish Running from Stopped. Keeping
    ///     the round-trip in one place means a future change to the OPC UA bit layout cannot
    ///     silently desync the probe-health decode.
    /// </summary>
    /// <param name="statusCode">The OPC UA status code to convert.</param>
    /// <returns>The OPC DA quality category byte.</returns>
    public static byte ToQualityCategoryByte(uint statusCode) =>
        (byte)(((statusCode >> 30) & 0x3u) switch
        {
            0u => 192u,   // Good   — top two bits 00b → OPC DA 0xC0
            1u => 64u,    // Uncertain — top two bits 01b → OPC DA 0x40
            _  => 0u,     // Bad    — top two bits 10b/11b → OPC DA 0x00
        });

    private static uint Categorize(byte q, ILogger? logger)
    {
        if (q >= 192) { Log(logger, q, "Good"); return Good; }
        if (q >= 64)  { Log(logger, q, "Uncertain"); return Uncertain; }
        Log(logger, q, "Bad");
        return Bad;
    }

    private static void Log(ILogger? logger, byte q, string bucket)
    {
        // Best-effort diagnostic so field captures can extend the table — once per bucket
        // per session is plenty (the LogWarning level is rate-limited by Serilog filters
        // in production).
        logger?.LogWarning(
            "Unrecognised MXAccess quality byte 0x{Q:X2} — falling back to {Bucket} category. " +
            "Field capture welcome — extend StatusCodeMap.FromQualityByte.", q, bucket);
    }
}
