using System.Globalization;
using System.Xml.Linq;

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

/// <summary>
///     The element/attribute reading rules shared by <see cref="MTConnectProbeParser"/> and
///     <see cref="MTConnectStreamsParser"/>. Both documents are the same dialect of XML and must be
///     read by the same rules; keeping one copy is what stops the two parsers drifting apart on the
///     two rules below, either of which fails silently rather than loudly.
/// </summary>
/// <remarks>
///     <para>
///         <b>Rule 1: elements are matched on <see cref="XName.LocalName"/>, ignoring the
///         namespace.</b> The namespace URI carries the MTConnect version
///         (<c>urn:mtconnect.org:MTConnectDevices:1.3</c> … <c>:2.0</c>), so a fully-qualified match
///         would hard-code a version. It also silently drops vendor-extension elements declared in
///         their own namespace, whose standard children still inherit the default MTConnect
///         namespace — the browse tree simply comes back short, with no error anywhere.
///     </para>
///     <para>
///         <b>Rule 2: attributes are read <i>unqualified</i>.</b> A prefixed attribute
///         (<c>xsi:type</c> on a document root, a vendor's <c>x:dataItemId</c>) must never be
///         mistaken for the real <c>type</c> / <c>dataItemId</c>, which would invent a data item the
///         probe never declared.
///     </para>
/// </remarks>
internal static class MTConnectXml
{
    /// <summary>Does <paramref name="element"/> have the given local name, whatever its namespace?</summary>
    /// <param name="element">The element to test.</param>
    /// <param name="localName">The unqualified name to match.</param>
    public static bool IsNamed(XElement element, string localName) =>
        string.Equals(element.Name.LocalName, localName, StringComparison.Ordinal);

    /// <summary>Direct element children of <paramref name="parent"/> with the given local name.</summary>
    /// <param name="parent">The element whose children to filter.</param>
    /// <param name="localName">The unqualified name to match.</param>
    public static IEnumerable<XElement> ChildrenNamed(XElement parent, string localName) =>
        parent.Elements().Where(child => IsNamed(child, localName));

    /// <summary>
    ///     Reads an unqualified attribute, normalizing a present-but-empty value to <c>null</c>.
    ///     See the type-level remarks for why only unqualified attributes are considered.
    /// </summary>
    /// <param name="element">The element carrying the attribute.</param>
    /// <param name="name">The unqualified attribute name.</param>
    public static string? OptionalAttribute(XElement element, string name)
    {
        var value = element.Attribute(name)?.Value;

        return string.IsNullOrWhiteSpace(value) ? null : value;
    }

    /// <summary>Reads a required unqualified attribute, or throws naming the owner and the attribute.</summary>
    /// <param name="element">The element carrying the attribute.</param>
    /// <param name="name">The unqualified attribute name.</param>
    /// <param name="owner">How to describe the element in the error message.</param>
    /// <param name="subject">How to describe the document in the error message (e.g. "/probe response").</param>
    /// <exception cref="InvalidDataException">The attribute is absent or blank.</exception>
    public static string RequiredAttribute(XElement element, string name, string owner, string subject)
    {
        var value = OptionalAttribute(element, name);

        return value ?? throw new InvalidDataException(
            $"MTConnect {subject} is malformed: {owner} has no '{name}' attribute.");
    }

    /// <summary>Reads an optional unqualified attribute as an <see cref="int"/>.</summary>
    /// <param name="element">The element carrying the attribute.</param>
    /// <param name="name">The unqualified attribute name.</param>
    /// <param name="owner">How to describe the element in the error message.</param>
    /// <param name="subject">How to describe the document in the error message.</param>
    /// <exception cref="InvalidDataException">The attribute is present but not an integer.</exception>
    public static int? OptionalIntAttribute(XElement element, string name, string owner, string subject)
    {
        var raw = OptionalAttribute(element, name);
        if (raw is null)
        {
            return null;
        }

        if (!int.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
        {
            throw new InvalidDataException(
                $"MTConnect {subject} is malformed: {owner} has a non-numeric '{name}' attribute ('{raw}').");
        }

        return value;
    }

    /// <summary>Reads a required unqualified attribute as a <see cref="long"/>.</summary>
    /// <param name="element">The element carrying the attribute.</param>
    /// <param name="name">The unqualified attribute name.</param>
    /// <param name="owner">How to describe the element in the error message.</param>
    /// <param name="subject">How to describe the document in the error message.</param>
    /// <exception cref="InvalidDataException">The attribute is absent, blank, or not an integer.</exception>
    public static long RequiredLongAttribute(XElement element, string name, string owner, string subject)
    {
        var raw = RequiredAttribute(element, name, owner, subject);

        if (!long.TryParse(raw, NumberStyles.Integer, CultureInfo.InvariantCulture, out var value))
        {
            throw new InvalidDataException(
                $"MTConnect {subject} is malformed: {owner} has a non-numeric '{name}' attribute ('{raw}').");
        }

        return value;
    }

    /// <summary>
    ///     Builds the error message for a document whose root is not what was expected, lifting the
    ///     Agent's own error text when the response is an <c>MTConnectError</c> document — which
    ///     Agents return under <b>HTTP 200</b>, so <c>EnsureSuccessStatusCode</c> never fires and
    ///     this is the only place the operator can learn what the Agent objected to.
    /// </summary>
    /// <param name="root">The document's actual root element.</param>
    /// <param name="expectedRootName">The root element name the caller required.</param>
    /// <param name="subject">How to describe the document in the error message.</param>
    public static string DescribeUnexpectedRoot(XElement root, string expectedRootName, string subject)
    {
        var message =
            $"MTConnect {subject} root element is <{root.Name.LocalName}>, expected <{expectedRootName}>.";

        if (!IsNamed(root, "MTConnectError"))
        {
            return message;
        }

        var errors = root.Descendants()
            .Where(e => IsNamed(e, "Error"))
            .Select(e =>
            {
                var code = OptionalAttribute(e, "errorCode");
                var text = e.Value.Trim();

                return code is null ? text : $"{code}: {text}";
            })
            .Where(text => text.Length > 0)
            .ToList();

        return errors.Count == 0
            ? message
            : $"{message} The Agent reported: {string.Join("; ", errors)}";
    }
}
