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

namespace ZB.MOM.WW.OtOpcUa.Driver.Sql.Contracts;

/// <summary>
///     Pure mapper from an authored raw tag's <c>TagConfig</c> JSON (design §5.2 / §5.3) to a
///     <see cref="SqlTagDefinition"/>, mirroring <c>ModbusTagDefinitionFactory.FromTagConfig</c>. The blob is
///     recognised by a leading <c>{</c> plus a <c>"driver":"Sql"</c> marker or a <c>"model"</c> discriminator;
///     the <c>model</c> and <c>type</c> enums are read with
///     <see cref="TagConfigJson.TryReadEnumStrict{TEnum}"/>, so a present-but-invalid (typo'd) value rejects
///     the whole tag — the driver then surfaces <c>BadNodeIdUnknown</c> instead of silently defaulting to a
///     different model and publishing a misleading <c>Good</c> (R2-11).
///     <para><b>No SQL is built here.</b> The parser only captures strings; the reader binds value fields as
///     <c>DbParameter</c>s and validates + quotes identifier fields. Tag input is never concatenated into a
///     command text, so a hostile <c>keyValue</c> is stored — and must be stored — verbatim.</para>
/// </summary>
public static class SqlEquipmentTagParser
{
    /// <summary>
    ///     Maps an authored <c>TagConfig</c> blob to a typed definition keyed by <paramref name="rawPath"/>.
    ///     Returns <see langword="false"/> — never throws — for anything the driver must not serve: a
    ///     non-object / unparseable blob, a blob belonging to another driver, a typo'd <c>model</c> or
    ///     <c>type</c> enum, a missing model-required field, or the P3-deferred
    ///     <see cref="SqlTagModel.Query"/> model.
    /// </summary>
    /// <param name="reference">The authored equipment-tag TagConfig JSON.</param>
    /// <param name="rawPath">The tag's RawPath — becomes the definition's identity (<c>Name</c>).</param>
    /// <param name="def">The mapped definition when this returns <see langword="true"/>.</param>
    /// <returns><see langword="true"/> when <paramref name="reference"/> is a valid Sql tag blob.</returns>
    public static bool TryParse(string reference, string rawPath, out SqlTagDefinition def)
    {
        def = null!;
        if (string.IsNullOrWhiteSpace(reference)) return false;
        if (reference.TrimStart().FirstOrDefault() != '{') return false;
        try
        {
            using var doc = JsonDocument.Parse(reference);
            var root = doc.RootElement;
            if (root.ValueKind != JsonValueKind.Object) return false;

            // Recognition: either the explicit driver marker or the model discriminator must be present,
            // so another driver's TagConfig blob is not mistaken for a Sql one.
            var driver = ReadString(root, "driver");
            var hasModel = root.TryGetProperty("model", out _);
            if (!hasModel && !string.Equals(driver, "Sql", StringComparison.OrdinalIgnoreCase)) return false;

            // Strict enum reads: absent ⇒ the fallback, present-but-invalid ⇒ reject the tag.
            if (!TagConfigJson.TryReadEnumStrict(root, "model", SqlTagModel.KeyValue, out var model)) return false;
            if (!TagConfigJson.TryReadEnumStrict(root, "type", default(DriverDataType), out var type)) return false;
            DriverDataType? declaredType =
                root.TryGetProperty("type", out var typeEl) && typeEl.ValueKind == JsonValueKind.String
                    ? type
                    : null;

            var table = ReadString(root, "table");
            if (string.IsNullOrWhiteSpace(table)) return false;
            var timestampColumn = ReadString(root, "timestampColumn");

            switch (model)
            {
                case SqlTagModel.KeyValue:
                {
                    var keyColumn = ReadString(root, "keyColumn");
                    var keyValue = ReadString(root, "keyValue");
                    var valueColumn = ReadString(root, "valueColumn");
                    if (string.IsNullOrWhiteSpace(keyColumn)
                        || string.IsNullOrWhiteSpace(valueColumn)
                        || keyValue is null)
                        return false;
                    def = new SqlTagDefinition(
                        Name: rawPath, Model: model, Table: table!,
                        KeyColumn: keyColumn, KeyValue: keyValue, ValueColumn: valueColumn,
                        TimestampColumn: timestampColumn, DeclaredType: declaredType);
                    return true;
                }

                case SqlTagModel.WideRow:
                {
                    var columnName = ReadString(root, "columnName");
                    if (string.IsNullOrWhiteSpace(columnName)) return false;
                    string? selectorColumn = null, selectorValue = null, topByTimestamp = null;
                    if (root.TryGetProperty("rowSelector", out var sel) && sel.ValueKind == JsonValueKind.Object)
                    {
                        selectorColumn = ReadString(sel, "whereColumn");
                        selectorValue = ReadScalar(sel, "whereValue");
                        topByTimestamp = ReadString(sel, "topByTimestamp");
                    }
                    // A wide-row tag must say WHICH row it reads — either a where-pair or newest-by-timestamp.
                    var hasWherePair = !string.IsNullOrWhiteSpace(selectorColumn) && selectorValue is not null;
                    if (!hasWherePair && string.IsNullOrWhiteSpace(topByTimestamp)) return false;
                    def = new SqlTagDefinition(
                        Name: rawPath, Model: model, Table: table!,
                        TimestampColumn: timestampColumn, ColumnName: columnName,
                        RowSelectorColumn: hasWherePair ? selectorColumn : null,
                        RowSelectorValue: hasWherePair ? selectorValue : null,
                        RowSelectorTopByTimestamp: hasWherePair ? null : topByTimestamp,
                        DeclaredType: declaredType);
                    return true;
                }

                default:
                    // SqlTagModel.Query is design §5.4 / P3 — deferred. Reject rather than serve a model
                    // the runtime cannot read.
                    return false;
            }
        }
        catch (JsonException) { return false; }
        catch (FormatException) { return false; }
    }

    /// <summary>Reads a string-valued property; null when absent or not a JSON string.</summary>
    private static string? ReadString(JsonElement o, string name)
        => o.TryGetProperty(name, out var e) && e.ValueKind == JsonValueKind.String ? e.GetString() : null;

    /// <summary>
    ///     Reads a scalar property as its textual form — a JSON string yields its contents, a number or
    ///     boolean yields its raw token — so an authored <c>whereValue</c> of either shape is captured
    ///     verbatim for later parameter binding. Null when absent or not a scalar.
    /// </summary>
    private static string? ReadScalar(JsonElement o, string name)
    {
        if (!o.TryGetProperty(name, out var e)) return null;
        return e.ValueKind switch
        {
            JsonValueKind.String => e.GetString(),
            JsonValueKind.Number or JsonValueKind.True or JsonValueKind.False => e.GetRawText(),
            _ => null,
        };
    }
}
