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

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

/// <summary>
///     Turns an MTConnect Agent <c>/probe</c> response (an <c>MTConnectDevices</c> XML document)
///     into the neutral <see cref="MTConnectProbeModel"/>. Deliberately a pure, socket-free static
///     so the whole parse is unit-testable against canned fixtures.
/// </summary>
/// <remarks>
///     <para>
///         <b>Why hand-rolled rather than TrakHound.</b> The TrakHound packages Task 0 pinned
///         (<c>MTConnect.NET-Common</c> + <c>-HTTP</c> 6.9.0.2 — both dropped in Task 7) contain <b>no</b>
///         <c>IResponseDocumentFormatter</c> implementation — the XML formatter ships in the
///         separate, unreferenced <c>MTConnect.NET-XML</c> package — so
///         <c>ResponseDocumentFormatter.CreateDevicesResponseDocument("xml", stream)</c> answers
///         <c>Success = false</c> / "Document Formatter Not found for &quot;xml&quot;" (verified by
///         reflection + live invocation against this repo's own fixture, 2026-07-24). TrakHound also
///         offers no socket-free public parse entry point, which the plan requires.
///     </para>
///     <para>
///         <b>Three shapes of the wire format this parser is built around.</b>
///         (1) A device may declare data items <i>directly</i> on <c>&lt;Device&gt;&lt;DataItems&gt;</c>,
///         outside any component — a walker that only recurses <c>&lt;Components&gt;</c> silently
///         drops them.
///         (2) The children of <c>&lt;Components&gt;</c> are named for the component <i>type</i>
///         (<c>&lt;Controller&gt;</c>, <c>&lt;Path&gt;</c>, <c>&lt;Axes&gt;</c>, …) — there is no
///         literal <c>&lt;Component&gt;</c> element — so <i>every</i> element child of
///         <c>&lt;Components&gt;</c> is a component.
///         (3) The document is XML-namespaced and the namespace URI carries the MTConnect version
///         (<c>urn:mtconnect.org:MTConnectDevices:1.3</c> … <c>:2.0</c>). Element matching and
///         attribute reading therefore go through <see cref="MTConnectXml"/>, whose two rules
///         (match on local name, read attributes unqualified) are shared verbatim with
///         <see cref="MTConnectStreamsParser"/> — see that type for why each rule exists.
///     </para>
///     <para>
///         <b>One deliberate divergence from the streams parser: no BOM/whitespace trim here.</b>
///         A <c>/probe</c> body arrives whole from <c>HttpContent</c>, so it begins exactly where
///         the Agent's document begins. A <c>/sample</c> chunk, by contrast, is lifted out of a
///         multipart frame and can carry a byte-order mark or the framing's own line break ahead of
///         the XML declaration, which <c>XmlReader</c> rejects — hence the trim there and not here.
///         This asymmetry is intentional, not an oversight.
///     </para>
///     <para>
///         <b>Failure posture.</b> Anything that is not a well-formed MTConnect device model throws
///         <see cref="InvalidDataException"/>. It never degrades to an empty-but-successful model:
///         an empty device model that looks successful is precisely the defect class that has bitten
///         this codebase before (#485), and here it would tear the driver's browse tree down to
///         nothing while reporting healthy.
///     </para>
/// </remarks>
internal static class MTConnectProbeParser
{
    /// <summary>
    ///     Bound on component nesting depth. Real device models nest a handful of levels; this only
    ///     exists so a pathological or hostile document cannot recurse the parser into a
    ///     process-fatal (uncatchable) stack overflow.
    /// </summary>
    private const int MaxComponentDepth = 64;

    /// <summary>How <see cref="MTConnectXml"/>'s shared error messages name this document.</summary>
    private const string Subject = "/probe response";

    /// <summary>Parses a <c>/probe</c> response held as a string.</summary>
    /// <param name="xml">The raw <c>MTConnectDevices</c> XML document.</param>
    /// <exception cref="InvalidDataException">
    ///     The payload is empty, not well-formed XML, not an <c>MTConnectDevices</c> document (an
    ///     Agent <c>MTConnectError</c> document included — Agents answer a bad device path with one
    ///     under HTTP 200), or declares no usable device.
    /// </exception>
    public static MTConnectProbeModel Parse(string xml)
    {
        if (string.IsNullOrWhiteSpace(xml))
        {
            throw new InvalidDataException("MTConnect /probe response was empty; expected an MTConnectDevices XML document.");
        }

        XDocument document;
        try
        {
            document = XDocument.Parse(xml);
        }
        catch (XmlException ex)
        {
            throw new InvalidDataException($"MTConnect /probe response is not well-formed XML: {ex.Message}", ex);
        }

        return Build(document);
    }

    /// <summary>Parses a <c>/probe</c> response read from a stream.</summary>
    /// <param name="stream">A stream positioned at the start of the XML document.</param>
    /// <exception cref="InvalidDataException">As for <see cref="Parse(string)"/>.</exception>
    public static MTConnectProbeModel Parse(Stream stream)
    {
        ArgumentNullException.ThrowIfNull(stream);

        XDocument document;
        try
        {
            document = XDocument.Load(stream);
        }
        catch (XmlException ex)
        {
            throw new InvalidDataException($"MTConnect /probe response is not well-formed XML: {ex.Message}", ex);
        }

        return Build(document);
    }

    private static MTConnectProbeModel Build(XDocument document)
    {
        var root = document.Root
                   ?? throw new InvalidDataException("MTConnect /probe response has no root element.");

        if (!MTConnectXml.IsNamed(root, "MTConnectDevices"))
        {
            throw new InvalidDataException(MTConnectXml.DescribeUnexpectedRoot(root, "MTConnectDevices", Subject));
        }

        var devicesContainer = MTConnectXml.ChildrenNamed(root, "Devices").FirstOrDefault()
                               ?? throw new InvalidDataException(
                                   "MTConnect /probe response has no <Devices> element; it does not describe a device model.");

        var devices = devicesContainer.Elements().Select(ReadDevice).ToList();
        if (devices.Count == 0)
        {
            throw new InvalidDataException(
                "MTConnect /probe response declares no devices; refusing to report an empty device model as a successful probe.");
        }

        return new MTConnectProbeModel(devices);
    }

    private static MTConnectDevice ReadDevice(XElement element) =>
        new(
            MTConnectXml.RequiredAttribute(element, "id", "Device", Subject),
            MTConnectXml.OptionalAttribute(element, "name"),
            ReadComponents(element, depth: 1),
            ReadDataItems(element));

    private static MTConnectComponent ReadComponent(XElement element, int depth) =>
        new(
            MTConnectXml.RequiredAttribute(element, "id", $"Component <{element.Name.LocalName}>", Subject),
            MTConnectXml.OptionalAttribute(element, "name"),
            ReadComponents(element, depth + 1),
            ReadDataItems(element));

    /// <summary>
    ///     Every element child of this container's <c>&lt;Components&gt;</c> element, whatever it is
    ///     named — the element name is the component type, not the literal string "Component".
    /// </summary>
    private static IReadOnlyList<MTConnectComponent> ReadComponents(XElement container, int depth)
    {
        if (depth > MaxComponentDepth)
        {
            throw new InvalidDataException(
                $"MTConnect /probe response nests components more than {MaxComponentDepth} levels deep; refusing to recurse further.");
        }

        return MTConnectXml.ChildrenNamed(container, "Components")
            .SelectMany(components => components.Elements())
            .Select(element => ReadComponent(element, depth))
            .ToList();
    }

    private static IReadOnlyList<MTConnectDataItem> ReadDataItems(XElement container) =>
        MTConnectXml.ChildrenNamed(container, "DataItems")
            .SelectMany(dataItems => MTConnectXml.ChildrenNamed(dataItems, "DataItem"))
            .Select(ReadDataItem)
            .ToList();

    private static MTConnectDataItem ReadDataItem(XElement element)
    {
        var id = MTConnectXml.RequiredAttribute(element, "id", "DataItem", Subject);

        return new MTConnectDataItem(
            id,
            MTConnectXml.OptionalAttribute(element, "name"),
            MTConnectXml.RequiredAttribute(element, "category", $"DataItem '{id}'", Subject),
            MTConnectXml.RequiredAttribute(element, "type", $"DataItem '{id}'", Subject),
            MTConnectXml.OptionalAttribute(element, "subType"),
            MTConnectXml.OptionalAttribute(element, "units"),
            MTConnectXml.OptionalAttribute(element, "representation"),
            MTConnectXml.OptionalIntAttribute(element, "sampleCount", $"DataItem '{id}'", Subject));
    }

}
