using System.Globalization;

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

/// <summary>
///     Parses classic Modicon address strings — both 5-digit (<c>40001</c>) and 6-digit
///     (<c>400001</c>) forms — into the protocol-level <see cref="ModbusRegion"/> +
///     zero-based PDU offset the driver speaks on the wire.
/// </summary>
/// <remarks>
///     <para>
///         Modicon notation uses a leading region digit (<c>0</c> coil, <c>1</c> discrete input,
///         <c>3</c> input register, <c>4</c> holding register) followed by a 1-based register
///         number. The two forms differ only in how many trailing digits encode the register
///         number: 5-digit caps at 9999, 6-digit at 65535. Both decode to the same wire
///         representation — the PDU offset is always 0..65535 — so the only meaningful
///         distinction is range coverage.
///     </para>
///     <para>
///         Foundational helper for the addressing grammar work tracked in
///         <c>docs/v2/modbus-addressing.md</c>. The richer suffix grammar (type / bit /
///         byte-order / array) layered on top in a follow-up calls into this parser to extract
///         the region + offset before processing modifiers.
///     </para>
/// </remarks>
public static class ModbusModiconAddress
{
    /// <summary>Parse a Modicon address string.</summary>
    /// <param name="address">Either 5-digit (<c>40001</c>) or 6-digit (<c>400001</c>) form.</param>
    /// <returns>Region + zero-based PDU offset the driver uses on the wire.</returns>
    /// <exception cref="FormatException">When the input is not a valid Modicon address.</exception>
    public static (ModbusRegion Region, ushort Offset) Parse(string address)
    {
        if (TryParse(address, out var region, out var offset, out var error))
            return (region, offset);
        throw new FormatException(error);
    }

    /// <summary>
    ///     Try-parse variant for hot-path / config-bind scenarios where a parse failure should
    ///     surface a structured diagnostic rather than throw. <paramref name="error"/> is
    ///     <c>null</c> on success.
    /// </summary>
    /// <param name="address">The address string to parse (5-digit or 6-digit form).</param>
    /// <param name="region">The parsed Modbus region, or default if parsing fails.</param>
    /// <param name="offset">The zero-based PDU offset, or 0 if parsing fails.</param>
    /// <param name="error">The error message if parsing fails, or null on success.</param>
    /// <returns><c>true</c> if the address parsed successfully; otherwise <c>false</c>.</returns>
    public static bool TryParse(string? address, out ModbusRegion region, out ushort offset, out string? error)
    {
        region = default;
        offset = 0;

        if (string.IsNullOrWhiteSpace(address))
        {
            error = "Modicon address is null or empty";
            return false;
        }

        // Range check up-front — keeps the rest of the parser straight-line. Modicon addresses
        // are exactly 5 or 6 characters: a leading region digit (0/1/3/4 — coils, discrete
        // inputs, input registers, holding registers respectively) followed by 4 (5-digit form)
        // or 5 (6-digit form) trailing digits encoding the 1-based register number. The
        // 5-digit form covers 1..9999 per region (e.g. coils 00001..09999, holding registers
        // 40001..49999); the 6-digit form covers the full 1..65536 wire range (e.g. coils
        // 000001..065536, holding 400001..465536). Anything else is unambiguously malformed so
        // we reject before doing the per-character work.
        var s = address.Trim();
        if (s.Length is not (5 or 6))
        {
            error = $"Modicon address must be 5 or 6 digits, got {s.Length} ('{address}')";
            return false;
        }

        if (!s.All(char.IsDigit))
        {
            error = $"Modicon address must contain only digits ('{address}')";
            return false;
        }

        var leading = s[0];
        region = leading switch
        {
            '0' => ModbusRegion.Coils,
            '1' => ModbusRegion.DiscreteInputs,
            '3' => ModbusRegion.InputRegisters,
            '4' => ModbusRegion.HoldingRegisters,
            _ => (ModbusRegion)(-1),
        };
        if ((int)region == -1)
        {
            error = $"Modicon address leading digit must be 0/1/3/4, got '{leading}'";
            return false;
        }

        // The remaining 4 (5-digit) or 5 (6-digit) digits are the 1-based register number.
        // 1-based-to-0-based conversion happens here so callers downstream uniformly see PDU
        // offsets — which is what the wire format actually uses.
        var registerNumberText = s[1..];
        if (!int.TryParse(registerNumberText, NumberStyles.None, CultureInfo.InvariantCulture, out var registerNumber))
        {
            error = $"Modicon register number is not a valid integer ('{registerNumberText}')";
            return false;
        }

        if (registerNumber < 1)
        {
            error = $"Modicon register number must be >= 1 (got {registerNumber})";
            return false;
        }

        // Wire-protocol maximum is register number 65536 (PDU offset 65535). The 5-digit form's
        // 4 trailing digits can only encode up to 9999, so this check is reached only by the
        // 6-digit form in practice — but it is applied to both for safety / simplicity rather
        // than relying on the digit-count invariant.
        if (registerNumber > 65536)
        {
            error = $"Modicon register number {registerNumber} exceeds the wire maximum (65536 / PDU offset 65535)";
            return false;
        }

        offset = (ushort)(registerNumber - 1);
        error = null;
        return true;
    }
}
