using System.Text.Json.Nodes;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;

/// <summary>Typed working model for an MTConnect tag's TagConfig JSON. The tag binds a single Agent
/// <c>DataItem</c> by its <c>id</c> attribute (<see cref="FullName"/> — MTConnect is read-only, so
/// there is no address encoding beyond the id); the remaining fields are probe-sourced display
/// metadata plus an author-settable type override. Preserves unrecognised JSON keys across a
/// load→save.</summary>
/// <remarks>
/// <para>
///     <see cref="MtCategory"/>/<see cref="MtType"/>/<see cref="MtSubType"/>/<see cref="Units"/> come
///     from the Agent's <c>/probe</c> response (see <c>MTConnectTagDefinition</c> in the driver's
///     Contracts project) and are shown read-only by Task 18's editor — this model still round-trips
///     them so a browse-commit that stamped them survives a manual edit of <see cref="DataType"/>.
///     <see cref="MtDevice"/>/<see cref="MtComponent"/> are pure author context (which physical
///     device/component this DataItem belongs to); nothing downstream consumes them.
/// </para>
/// <para>
///     <see cref="DataType"/> is an override of the driver's own probe-based inference
///     (<c>MTConnectDataTypeInference.Infer</c>, which the browse-commit path already applied when it
///     wrote <see cref="FullName"/> and this field). It is deliberately NOT recomputed here — this
///     editor only re-serialises whatever the operator picks or the browse commit stamped, never a
///     locally-reinvented inference that could disagree with the one true rule.
/// </para>
/// </remarks>
public sealed class MTConnectTagConfigModel
{
    /// <summary>The MTConnect DataItem's <c>id</c> attribute — the driver's read/subscribe key. Required.
    /// Loaded from <c>fullName</c>, or from the driver's alternate spellings <c>dataItemId</c> /
    /// <c>address</c> (see <see cref="IdKeys"/>); always saved as <c>fullName</c>.</summary>
    public string FullName { get; set; } = "";

    /// <summary>Logical data type override for the DataItem's value. Defaults to <see cref="DriverDataType.String"/>
    /// — MTConnect is weakly typed on the wire, so an unset override should never coerce to a numeric type
    /// that can fail to parse (mirrors <c>MTConnectDataTypeInference</c>'s own String-leaning default).</summary>
    public DriverDataType DataType { get; set; } = DriverDataType.String;

    /// <summary>The DataItem's probe-sourced <c>category</c> (<c>SAMPLE</c>/<c>EVENT</c>/<c>CONDITION</c>). Read-only display metadata.</summary>
    public string? MtCategory { get; set; }

    /// <summary>The DataItem's probe-sourced <c>type</c> (e.g. <c>POSITION</c>, <c>EXECUTION</c>). Read-only display metadata.</summary>
    public string? MtType { get; set; }

    /// <summary>The DataItem's probe-sourced <c>subType</c> (e.g. <c>ACTUAL</c>, <c>COMMANDED</c>). Read-only display metadata.</summary>
    public string? MtSubType { get; set; }

    /// <summary>The DataItem's probe-sourced <c>units</c> (e.g. <c>MILLIMETER</c>). Read-only display metadata.</summary>
    public string? Units { get; set; }

    /// <summary>Author-entered note identifying the owning MTConnect device. Pure authoring context; not
    /// consumed by the driver or the runtime.</summary>
    public string? MtDevice { get; set; }

    /// <summary>Author-entered note identifying the owning MTConnect component. Pure authoring context; not
    /// consumed by the driver or the runtime.</summary>
    public string? MtComponent { get; set; }

    // The as-loaded raw "dataType" JSON value, kept only to detect the ParseEnum numeric-text trap in
    // Validate() below — Enum.TryParse (which TagConfigJson.GetEnum uses) silently accepts a quoted
    // number ("8" -> Float64) exactly as readily as a name, so a config that was ever hand-authored or
    // migrated with a numeric dataType would otherwise load into a *valid-looking* enum value with no
    // signal that it isn't a name. ToJson() always re-writes DataType as a name, so this can only ever
    // be non-null on the FIRST load of an already-poisoned config, never after a save through this model.
    private string? _rawDataType;

    private JsonObject _bag = new();

    /// <summary>
    ///     The accepted spellings of the DataItem id, in precedence order, mirroring the driver's own
    ///     <c>MTConnectRawTagConfigDto</c>: <c>fullName</c> (this model's canonical key),
    ///     <c>dataItemId</c> (what an operator hand-authoring the blob reaches for), and <c>address</c>
    ///     (what <c>RawBrowseCommitMapper</c> wrote for MTConnect before it grew a typed-editor branch).
    ///     Reading only <c>fullName</c> made every already-committed tag open with an EMPTY id field and
    ///     get blanked on save, while the data plane kept working — the symptom looked cosmetic.
    ///     <see cref="ToJson"/> normalises onto <c>fullName</c> and drops the aliases, so the two
    ///     spellings can never drift apart on a later edit.
    /// </summary>
    private static readonly string[] IdKeys = ["fullName", "dataItemId", "address"];

    /// <summary>Loads a model from a TagConfig JSON string, defaulting any absent field and retaining
    /// every original key (so fields this editor doesn't expose survive a load→save).</summary>
    /// <param name="json">The raw TagConfig JSON string, or <c>null</c> for a new/empty config.</param>
    /// <returns>The populated <see cref="MTConnectTagConfigModel"/>.</returns>
    public static MTConnectTagConfigModel FromJson(string? json)
    {
        var o = TagConfigJson.ParseOrNew(json);
        return new MTConnectTagConfigModel
        {
            FullName = ReadId(o),
            DataType = TagConfigJson.GetEnum(o, "dataType", DriverDataType.String),
            MtCategory = TagConfigJson.GetString(o, "mtCategory"),
            MtType = TagConfigJson.GetString(o, "mtType"),
            MtSubType = TagConfigJson.GetString(o, "mtSubType"),
            Units = TagConfigJson.GetString(o, "units"),
            MtDevice = TagConfigJson.GetString(o, "mtDevice"),
            MtComponent = TagConfigJson.GetString(o, "mtComponent"),
            _rawDataType = TagConfigJson.GetString(o, "dataType"),
            _bag = o,
        };
    }

    /// <summary>Serialises this model back to a TagConfig JSON string over the preserved key bag.
    /// <c>dataType</c> is always written as its enum NAME (never a bare number — see the
    /// enum-serialization trap in the class remarks); the optional metadata keys are omitted when
    /// blank rather than persisted as empty strings.</summary>
    /// <returns>The serialised TagConfig JSON string.</returns>
    public string ToJson()
    {
        // Normalise onto the canonical key: a blob loaded via an alias must not keep the alias, or the
        // two spellings drift on the next edit and the driver silently reads the stale one.
        TagConfigJson.Set(_bag, "dataItemId", null);
        TagConfigJson.Set(_bag, "address", null);
        TagConfigJson.Set(_bag, "fullName", FullName.Trim());
        TagConfigJson.Set(_bag, "dataType", DataType);
        TagConfigJson.Set(_bag, "mtCategory", Blank(MtCategory));
        TagConfigJson.Set(_bag, "mtType", Blank(MtType));
        TagConfigJson.Set(_bag, "mtSubType", Blank(MtSubType));
        TagConfigJson.Set(_bag, "units", Blank(Units));
        TagConfigJson.Set(_bag, "mtDevice", Blank(MtDevice));
        TagConfigJson.Set(_bag, "mtComponent", Blank(MtComponent));
        return TagConfigJson.Serialize(_bag);
    }

    /// <summary>Validation hook; returns an error message or null when the model is valid.</summary>
    /// <returns>An error message describing the validation failure, or <c>null</c> when the model is valid.</returns>
    public string? Validate()
    {
        if (string.IsNullOrWhiteSpace(FullName))
        {
            return "A DataItem id (fullName) is required.";
        }

        // See the _rawDataType remarks: a quoted number parses as a "valid" enum today but is never
        // something this editor itself would have written, so surface it instead of silently accepting it.
        if (_rawDataType is { Length: > 0 } raw && raw.All(char.IsAsciiDigit))
        {
            return $"dataType \"{raw}\" is a numeric value, not a named type — re-select a data type.";
        }

        return null;
    }

    // Normalises a blank/whitespace-only string to null so TagConfigJson.Set omits the key rather than
    // persisting an empty string.
    private static string? Blank(string? value) => string.IsNullOrWhiteSpace(value) ? null : value;

    // First non-blank id spelling wins (see IdKeys). A present-but-blank "fullName" must not shadow a
    // populated alias — that is exactly the state a save from the broken editor left behind.
    private static string ReadId(JsonObject o)
    {
        foreach (var key in IdKeys)
        {
            if (Blank(TagConfigJson.GetString(o, key)) is { } id)
            {
                return id;
            }
        }

        return "";
    }
}
