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

namespace ZB.MOM.WW.ScadaBridge.NotificationService.Ews;

/// <summary>
/// The shape of an EWS response body. This says what Exchange answered, not
/// whether the failure is worth retrying — that judgement belongs to the sender's classifier.
/// </summary>
public enum EwsResponseKind
{
    /// <summary>The <c>CreateItemResponseMessage</c> reported <c>ResponseClass="Success"</c>.</summary>
    Success,

    /// <summary>The <c>CreateItemResponseMessage</c> reported a non-success response class.</summary>
    Error,

    /// <summary>The body carried a SOAP <c>Fault</c> instead of a response message.</summary>
    Fault,

    /// <summary>The body was not XML, was empty, or was XML of no recognised EWS shape.</summary>
    Unparseable,
}

/// <summary>
/// The outcome of parsing an EWS response body.
/// </summary>
/// <param name="Kind">The response shape.</param>
/// <param name="ResponseCode">
/// The EWS response code (for example <c>NoError</c>, <c>ErrorServerBusy</c>), or <c>null</c>
/// when the body carried none.
/// </param>
/// <param name="MessageText">The human-readable message or SOAP fault string, when present.</param>
public sealed record EwsParseResult(EwsResponseKind Kind, string? ResponseCode, string? MessageText);

/// <summary>
/// Parses an EWS <c>CreateItem</c> response body into a <see cref="EwsParseResult"/>.
/// <para>
/// Pure and judgement-free by design: it reports the response shape and code and never decides
/// transient versus permanent, so the retry policy stays in one place in the sender.
/// </para>
/// </summary>
public static class EwsResponseParser
{
    private static readonly XNamespace Soap = "http://schemas.xmlsoap.org/soap/envelope/";
    private static readonly XNamespace Messages = "http://schemas.microsoft.com/exchange/services/2006/messages";

    private static readonly EwsParseResult UnparseableResult =
        new(EwsResponseKind.Unparseable, ResponseCode: null, MessageText: null);

    /// <summary>
    /// Parses a response body. A malformed or unrecognised payload is reported as
    /// <see cref="EwsResponseKind.Unparseable"/> rather than throwing — a broken response must not
    /// take out the delivery path with an unhandled exception.
    /// </summary>
    /// <param name="responseBody">The raw HTTP response body returned by the EWS endpoint.</param>
    /// <returns>The parsed shape, response code and message text.</returns>
    public static EwsParseResult Parse(string responseBody)
    {
        if (string.IsNullOrWhiteSpace(responseBody))
        {
            return UnparseableResult;
        }

        XDocument document;
        try
        {
            // The body is external input, so DTDs are prohibited outright: LINQ-to-XML's own
            // XDocument.Parse permits an internal DTD subset and expands its entities (verified),
            // which is an entity-expansion DoS on a response body. A null resolver additionally
            // blocks external entity/DTD fetches (XXE).
            var settings = new XmlReaderSettings
            {
                DtdProcessing = DtdProcessing.Prohibit,
                XmlResolver = null,
            };

            using var reader = XmlReader.Create(new StringReader(responseBody), settings);
            document = XDocument.Load(reader);
        }
        catch (XmlException)
        {
            return UnparseableResult;
        }

        var responseMessage = document.Descendants(Messages + "CreateItemResponseMessage").FirstOrDefault();
        if (responseMessage is not null)
        {
            var responseClass = (string?)responseMessage.Attribute("ResponseClass");
            var kind = string.Equals(responseClass, "Success", StringComparison.Ordinal)
                ? EwsResponseKind.Success
                : EwsResponseKind.Error;

            return new EwsParseResult(
                kind,
                responseMessage.Element(Messages + "ResponseCode")?.Value,
                responseMessage.Element(Messages + "MessageText")?.Value);
        }

        var fault = document.Descendants(Soap + "Fault").FirstOrDefault();
        if (fault is not null)
        {
            // faultstring is unqualified in SOAP 1.1, and the detail's ResponseCode appears under
            // several vendor namespaces (errors/types) depending on the fault — match on local name.
            var faultString = fault.Descendants()
                .FirstOrDefault(e => e.Name.LocalName == "faultstring")?.Value;

            var detail = fault.Descendants().FirstOrDefault(e => e.Name.LocalName == "detail");
            var responseCode = detail?.Descendants()
                .FirstOrDefault(e => e.Name.LocalName == "ResponseCode")?.Value;

            return new EwsParseResult(EwsResponseKind.Fault, responseCode, faultString);
        }

        return UnparseableResult;
    }
}
