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

/// <summary>
///     Documented-API capability matrix — per CNC series, what ranges each
///     <see cref="FocasAreaKind"/> supports. Authoritative source for the driver's
///     pre-flight validation in <see cref="FocasDriver.InitializeAsync"/>.
/// </summary>
/// <remarks>
///     <para>Ranges come from the Fanuc FOCAS Developer Kit documentation matrix
///     (see <c>docs/v2/focas-version-matrix.md</c> for the authoritative copy with
///     per-function citations). Numbers chosen to match what the FOCAS library
///     accepts — a read against an address outside the documented range returns
///     <c>EW_NUMBER</c> or <c>EW_PARAM</c> at the wire, which this driver maps to
///     BadOutOfRange. Catching at init time surfaces the mismatch as a config
///     error before any session is opened.</para>
///     <para><see cref="FocasCncSeries.Unknown"/> is treated permissively: every
///     address passes validation. Pre-matrix configs don't break on upgrade; new
///     deployments are encouraged to declare a series in the device options.</para>
/// </remarks>
public static class FocasCapabilityMatrix
{
    /// <summary>
    ///     Validates whether an address is accepted by a CNC of the given series.
    ///     Returns null on pass, or a failure reason on reject.
    /// </summary>
    /// <param name="series">The CNC series to validate against.</param>
    /// <param name="address">The address to validate.</param>
    /// <returns>Null if valid, otherwise a failure reason string.</returns>
    public static string? Validate(FocasCncSeries series, FocasAddress address)
    {
        if (series == FocasCncSeries.Unknown) return null;

        return address.Kind switch
        {
            FocasAreaKind.Macro     => ValidateMacro(series, address.Number),
            FocasAreaKind.Parameter => ValidateParameter(series, address.Number),
            FocasAreaKind.Pmc       => ValidatePmc(series, address.PmcLetter, address.Number),
            _ => null,
        };
    }

    /// <summary>Gets the macro variable number range accepted by a CNC series.</summary>
    /// <param name="series">The CNC series.</param>
    /// <returns>A tuple of (min, max) macro numbers.</returns>
    internal static (int min, int max) MacroRange(FocasCncSeries series) => series switch
    {
        // Common macros 1-33 + 100-199 + 500-999 universally; extended 10000+ only on
        // higher-end series. Using the extended ceiling per series per DevKit notes.
        FocasCncSeries.Sixteen_i    => (0, 999),
        FocasCncSeries.Zero_i_D     => (0, 999),
        FocasCncSeries.Zero_i_F or
        FocasCncSeries.Zero_i_MF or
        FocasCncSeries.Zero_i_TF    => (0, 9999),
        FocasCncSeries.Thirty_i or
        FocasCncSeries.ThirtyOne_i or
        FocasCncSeries.ThirtyTwo_i  => (0, 99999),
        FocasCncSeries.PowerMotion_i => (0, 999),
        _ => (0, int.MaxValue),
    };

    /// <summary>Gets the parameter number range accepted by a CNC series.</summary>
    /// <param name="series">The CNC series.</param>
    /// <returns>A tuple of (min, max) parameter numbers.</returns>
    internal static (int min, int max) ParameterRange(FocasCncSeries series) => series switch
    {
        FocasCncSeries.Sixteen_i    => (0, 9999),
        FocasCncSeries.Zero_i_D or
        FocasCncSeries.Zero_i_F or
        FocasCncSeries.Zero_i_MF or
        FocasCncSeries.Zero_i_TF    => (0, 14999),
        FocasCncSeries.Thirty_i or
        FocasCncSeries.ThirtyOne_i or
        FocasCncSeries.ThirtyTwo_i  => (0, 29999),
        FocasCncSeries.PowerMotion_i => (0, 29999),
        _ => (0, int.MaxValue),
    };

    /// <summary>Gets the PMC letters accepted by a CNC series.</summary>
    /// <param name="series">The CNC series.</param>
    /// <returns>A set of accepted PMC letter strings.</returns>
    internal static IReadOnlySet<string> PmcLetters(FocasCncSeries series) => series switch
    {
        FocasCncSeries.Sixteen_i     => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "R", "D" },
        FocasCncSeries.Zero_i_D      => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "R", "D", "E", "A" },
        FocasCncSeries.Zero_i_F or
        FocasCncSeries.Zero_i_MF or
        FocasCncSeries.Zero_i_TF     => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "F", "G", "R", "D", "E", "A", "M", "C" },
        FocasCncSeries.Thirty_i or
        FocasCncSeries.ThirtyOne_i or
        FocasCncSeries.ThirtyTwo_i   => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "F", "G", "R", "D", "E", "A", "M", "C", "K", "T" },
        FocasCncSeries.PowerMotion_i => new HashSet<string>(StringComparer.OrdinalIgnoreCase) { "X", "Y", "R", "D" },
        _ => new HashSet<string>(StringComparer.OrdinalIgnoreCase),
    };

    /// <summary>Gets the maximum PMC address number for a CNC series.</summary>
    /// <param name="series">The CNC series.</param>
    /// <returns>The maximum address number.</returns>
    internal static int PmcMaxNumber(FocasCncSeries series) => series switch
    {
        FocasCncSeries.Sixteen_i    => 999,
        FocasCncSeries.Zero_i_D     => 1999,
        FocasCncSeries.Zero_i_F or
        FocasCncSeries.Zero_i_MF or
        FocasCncSeries.Zero_i_TF    => 9999,
        FocasCncSeries.Thirty_i or
        FocasCncSeries.ThirtyOne_i or
        FocasCncSeries.ThirtyTwo_i  => 59999,
        FocasCncSeries.PowerMotion_i => 1999,
        _ => int.MaxValue,
    };

    private static string? ValidateMacro(FocasCncSeries series, int number)
    {
        var (min, max) = MacroRange(series);
        return (number < min || number > max)
            ? $"Macro variable #{number} is outside the documented range [{min}, {max}] for {series}."
            : null;
    }

    private static string? ValidateParameter(FocasCncSeries series, int number)
    {
        var (min, max) = ParameterRange(series);
        return (number < min || number > max)
            ? $"Parameter #{number} is outside the documented range [{min}, {max}] for {series}."
            : null;
    }

    private static string? ValidatePmc(FocasCncSeries series, string? letter, int number)
    {
        if (string.IsNullOrEmpty(letter)) return "PMC address is missing its letter prefix.";
        var letters = PmcLetters(series);
        if (!letters.Contains(letter))
        {
            var letterList = string.Join(", ", letters);
            return $"PMC letter '{letter}' is not supported on {series}. Accepted: {{{letterList}}}.";
        }
        var max = PmcMaxNumber(series);
        return number > max
            ? $"PMC address {letter}{number} is outside the documented range [0, {max}] for {series}."
            : null;
    }
}
