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

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

/// <summary>
/// Typed working model for a read-only Sql tag's TagConfig JSON (the driver-specific table/column
/// mapping; name/writable live on the Tag entity). Mirrors — byte-for-byte on the fields it owns — the
/// authoritative runtime reader <see cref="SqlEquipmentTagParser.TryParse"/>: the <c>model</c> and
/// <c>type</c> enums are written as their NAME strings (never numbers), and <see cref="Validate"/>
/// enforces the same required-field/selector shape the parser accepts. Preserves unrecognised JSON keys
/// (top-level and inside <c>rowSelector</c>) across a load→save so a future field survives an edit.
/// </summary>
public sealed class SqlTagConfigModel
{
    /// <summary>Tag→value mapping model. v1 accepts <see cref="SqlTagModel.KeyValue"/> and
    /// <see cref="SqlTagModel.WideRow"/>; <see cref="SqlTagModel.Query"/> is deferred and rejected.</summary>
    public SqlTagModel Model { get; set; } = SqlTagModel.KeyValue;

    /// <summary>The table or view to read (e.g. <c>dbo.TagValues</c>). Required for every model.</summary>
    public string? Table { get; set; }

    /// <summary><see cref="SqlTagModel.KeyValue"/>: the column matched against <see cref="KeyValue"/>.</summary>
    public string? KeyColumn { get; set; }

    /// <summary><see cref="SqlTagModel.KeyValue"/>: this tag's key value (bound as a parameter at read time).</summary>
    public string? KeyValue { get; set; }

    /// <summary><see cref="SqlTagModel.KeyValue"/>: the column the value is read from.</summary>
    public string? ValueColumn { get; set; }

    /// <summary>Optional source-timestamp column; absent ⇒ the driver stamps the poll time. Both models.</summary>
    public string? TimestampColumn { get; set; }

    /// <summary><see cref="SqlTagModel.WideRow"/>: the column this tag reads from the selected row.</summary>
    public string? ColumnName { get; set; }

    /// <summary><see cref="SqlTagModel.WideRow"/>: which row to read — a where-pair or newest-by-timestamp.</summary>
    public SqlRowSelectorModel RowSelector { get; set; } = new();

    /// <summary>Optional explicit OPC UA data type override (the blob's <c>type</c> field). <c>null</c> ⇒ the
    /// driver infers the type from the result-set column metadata.</summary>
    public DriverDataType? Type { get; set; }

    private JsonObject _bag = new();

    // The raw offending string when "model"/"type" is present-but-invalid (a typo). Mirrors the parser's
    // strict-enum reject: absent OR present-non-string ⇒ null (nothing to validate).
    private string? _invalidModelRaw;
    private string? _invalidTypeRaw;

    /// <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="SqlTagConfigModel"/>.</returns>
    public static SqlTagConfigModel FromJson(string? json)
    {
        var o = TagConfigJson.ParseOrNew(json);
        var (model, invalidModel) = ReadEnumStrict<SqlTagModel>(o, "model");
        var (type, invalidType) = ReadEnumStrict<DriverDataType>(o, "type");

        var selectorObj = o.TryGetPropertyValue("rowSelector", out var sn) && sn is JsonObject so ? so : null;

        return new SqlTagConfigModel
        {
            Model = model ?? SqlTagModel.KeyValue,
            Table = TagConfigJson.GetString(o, "table"),
            KeyColumn = TagConfigJson.GetString(o, "keyColumn"),
            KeyValue = TagConfigJson.GetString(o, "keyValue"),
            ValueColumn = TagConfigJson.GetString(o, "valueColumn"),
            TimestampColumn = TagConfigJson.GetString(o, "timestampColumn"),
            ColumnName = TagConfigJson.GetString(o, "columnName"),
            RowSelector = SqlRowSelectorModel.FromJson(selectorObj),
            Type = type,
            _bag = o,
            _invalidModelRaw = invalidModel,
            _invalidTypeRaw = invalidType,
        };
    }

    /// <summary>Serialises this model back to a TagConfig JSON string, writing the exposed fields (enums as
    /// their name strings) over the preserved key bag. The nested <c>rowSelector</c> object is rebuilt from
    /// <see cref="RowSelector"/> (preserving its own unknown keys) and omitted entirely when empty.</summary>
    /// <returns>The serialised TagConfig JSON string.</returns>
    public string ToJson()
    {
        TagConfigJson.Set(_bag, "model", Model);
        TagConfigJson.Set(_bag, "table", Table);
        TagConfigJson.Set(_bag, "keyColumn", KeyColumn);
        TagConfigJson.Set(_bag, "keyValue", KeyValue);
        TagConfigJson.Set(_bag, "valueColumn", ValueColumn);
        TagConfigJson.Set(_bag, "timestampColumn", TimestampColumn);
        TagConfigJson.Set(_bag, "columnName", ColumnName);
        TagConfigJson.Set(_bag, "type", Type);

        var selector = RowSelector.ToJsonObject();
        if (selector is null) { _bag.Remove("rowSelector"); }
        else { _bag["rowSelector"] = selector; }

        return TagConfigJson.Serialize(_bag);
    }

    /// <summary>Validates the model against the exact accept/reject boundary of
    /// <see cref="SqlEquipmentTagParser.TryParse"/>. Returns an error message, or <c>null</c> when valid.</summary>
    /// <returns>An error message describing the validation failure, or <c>null</c> when the model is valid.</returns>
    public string? Validate()
    {
        // Strict enums (mirrors the parser's TryReadEnumStrict): a present-but-invalid string rejects the tag.
        if (_invalidModelRaw is not null) { return $"model: '{_invalidModelRaw}' is not a valid Sql tag model."; }
        if (_invalidTypeRaw is not null) { return $"type: '{_invalidTypeRaw}' is not a valid data type."; }

        // Query is design §5.4 / P3 — the parser rejects it; the editor must too.
        if (Model == SqlTagModel.Query) { return "model 'Query' is deferred (P3) and not supported."; }

        if (string.IsNullOrWhiteSpace(Table)) { return "table is required."; }

        switch (Model)
        {
            case SqlTagModel.KeyValue:
                if (string.IsNullOrWhiteSpace(KeyColumn)) { return "keyColumn is required for a KeyValue tag."; }
                if (string.IsNullOrWhiteSpace(ValueColumn)) { return "valueColumn is required for a KeyValue tag."; }
                if (KeyValue is null) { return "keyValue is required for a KeyValue tag."; }
                return null;

            case SqlTagModel.WideRow:
                if (string.IsNullOrWhiteSpace(ColumnName)) { return "columnName is required for a WideRow tag."; }
                // A where-pair needs BOTH whereColumn and whereValue (whereValue may be an empty string, but
                // must be present) — mirrors the parser's `!blank(whereColumn) && whereValue is not null`.
                var hasWherePair = !string.IsNullOrWhiteSpace(RowSelector.WhereColumn) && RowSelector.WhereValue is not null;
                if (!hasWherePair && string.IsNullOrWhiteSpace(RowSelector.TopByTimestamp))
                {
                    return "a WideRow tag needs a row selector: a whereColumn/whereValue pair, or topByTimestamp.";
                }
                return null;

            default:
                return "model is not a supported Sql tag model.";
        }
    }

    // Reads an enum-valued string field the way the parser's TryReadEnumStrict does: absent OR present-but-
    // non-string ⇒ (null, null) — nothing to validate; a present string that parses ⇒ (value, null); a
    // present string that does NOT parse ⇒ (null, offendingString) so Validate can reject it.
    private static (TEnum? value, string? invalidRaw) ReadEnumStrict<TEnum>(JsonObject o, string name)
        where TEnum : struct, Enum
    {
        if (!o.TryGetPropertyValue(name, out var n) || n is not JsonValue v || !v.TryGetValue<string>(out var s))
        {
            return (null, null);
        }
        return Enum.TryParse<TEnum>(s, ignoreCase: true, out var parsed) ? (parsed, null) : (null, s);
    }
}

/// <summary>
/// Typed working model for a <see cref="SqlTagModel.WideRow"/> tag's nested <c>rowSelector</c> object.
/// Holds a where-pair (<see cref="WhereColumn"/>/<see cref="WhereValue"/>) or a newest-by-timestamp
/// selector (<see cref="TopByTimestamp"/>). Preserves unknown keys inside the selector across load→save.
/// </summary>
public sealed class SqlRowSelectorModel
{
    /// <summary>The column that picks the row (matched against <see cref="WhereValue"/>).</summary>
    public string? WhereColumn { get; set; }

    /// <summary>The value <see cref="WhereColumn"/> is matched against (bound as a parameter at read time).
    /// Captured as text whether the blob authored it as a JSON string or a JSON number/boolean, mirroring
    /// the parser's scalar read.</summary>
    public string? WhereValue { get; set; }

    /// <summary>Newest-row selector: the timestamp column to order by (used when no where-pair is authored).</summary>
    public string? TopByTimestamp { get; set; }

    private JsonObject _bag = new();

    /// <summary>Loads a selector model from the nested <c>rowSelector</c> JSON object (or a fresh empty model
    /// when the object is absent), retaining any unknown keys.</summary>
    /// <param name="o">The nested <c>rowSelector</c> object, or <c>null</c> when absent.</param>
    /// <returns>The populated <see cref="SqlRowSelectorModel"/>.</returns>
    public static SqlRowSelectorModel FromJson(JsonObject? o)
    {
        // Deep-clone so the selector owns an independent node tree (safe to re-attach on ToJson).
        var bag = o?.DeepClone().AsObject() ?? new JsonObject();
        return new SqlRowSelectorModel
        {
            WhereColumn = TagConfigJson.GetString(bag, "whereColumn"),
            WhereValue = ReadScalarText(bag, "whereValue"),
            TopByTimestamp = TagConfigJson.GetString(bag, "topByTimestamp"),
            _bag = bag,
        };
    }

    /// <summary>Rebuilds the nested <c>rowSelector</c> JSON object from the exposed fields over the preserved
    /// key bag, or returns <c>null</c> when the selector carries nothing (so the parent omits the key).</summary>
    /// <returns>The <c>rowSelector</c> <see cref="JsonObject"/>, or <c>null</c> when empty.</returns>
    public JsonObject? ToJsonObject()
    {
        var o = _bag.DeepClone().AsObject();
        TagConfigJson.Set(o, "whereColumn", WhereColumn);
        TagConfigJson.Set(o, "whereValue", WhereValue);
        TagConfigJson.Set(o, "topByTimestamp", TopByTimestamp);
        return o.Count == 0 ? null : o;
    }

    // Reads a scalar as its textual form: a JSON string yields its contents, a number/boolean yields its raw
    // token — so an authored whereValue of either shape is captured verbatim (mirrors the parser's ReadScalar).
    private static string? ReadScalarText(JsonObject o, string name)
    {
        if (!o.TryGetPropertyValue(name, out var n) || n is not JsonValue v) { return null; }
        return v.TryGetValue<string>(out var s) ? s : v.ToJsonString();
    }
}
