using System.Globalization;
using System.Text.Json.Nodes;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.AbCip;
using ZB.MOM.WW.OtOpcUa.Driver.AbLegacy;
using ZB.MOM.WW.OtOpcUa.Driver.FOCAS;
using ZB.MOM.WW.OtOpcUa.Driver.Modbus;
using ZB.MOM.WW.OtOpcUa.Driver.S7;
using ZB.MOM.WW.OtOpcUa.Driver.TwinCAT;

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

/// <summary>
/// Metadata for one per-driver <b>typed</b> CSV column: its CSV header, the root TagConfig JSON key it
/// maps to, and — for the model-backed maps — the name of the property on the driver's
/// <c><![CDATA[<Driver>TagConfigModel]]></c> it corresponds to. <see cref="ModelProperty"/> is the anchor
/// the CSV column-map reflection guard test asserts against so a driver-model rename can never silently
/// drift the CSV surface. It is <c>null</c> for the model-less maps (Galaxy / Calculation), which map
/// a header directly onto a JSON key.
/// </summary>
/// <param name="Header">The CSV column header (PascalCase, e.g. <c>ModbusDataType</c>).</param>
/// <param name="JsonKey">The root TagConfig JSON key this column reads/writes (camelCase, e.g. <c>dataType</c>).</param>
/// <param name="ModelProperty">The backing model property name, or <c>null</c> for a model-less map.</param>
public sealed record CsvTypedColumn(string Header, string JsonKey, string? ModelProperty);

/// <summary>
/// Per-driver mapping between a CSV row's <b>typed</b> columns and a tag's driver-specific
/// <c>TagConfig</c> JSON. The map is the inverse pair the WP5 CSV import/export path pivots on:
/// <see cref="ApplyTypedColumns"/> folds a row's typed columns onto a base TagConfig (typed column wins
/// on conflict); <see cref="ReadTypedColumns"/> projects a stored TagConfig back to its typed cells for
/// export. Lives next to the <c><![CDATA[<Driver>TagConfigModel]]></c> types so model + map evolve
/// together.
/// </summary>
public abstract class CsvDriverTagMap
{
    /// <summary>The driver-type string this map serves (a <see cref="DriverTypeNames"/> value).</summary>
    public abstract string DriverType { get; }

    /// <summary>The backing model type whose scalar surface this map mirrors, or <c>null</c> for a
    /// model-less (raw-JSON-key) map (Galaxy / Calculation).</summary>
    public abstract Type? ModelType { get; }

    /// <summary>The ordered typed columns this driver adds to the common CSV column set.</summary>
    public abstract IReadOnlyList<CsvTypedColumn> Columns { get; }

    /// <summary>The typed column headers, in order.</summary>
    public IReadOnlyList<string> Headers => Columns.Select(c => c.Header).ToArray();

    /// <summary>The root TagConfig JSON keys this map owns (used to compute the export residual).</summary>
    public IReadOnlyList<string> JsonKeys => Columns.Select(c => c.JsonKey).ToArray();

    /// <summary>
    /// Folds the typed columns present (non-blank) in <paramref name="row"/> onto
    /// <paramref name="baseJson"/> and returns the merged driver TagConfig JSON. The base is loaded first
    /// (so its unrecognised keys survive) and each present typed column then overrides its key — so a
    /// typed column <b>wins</b> over a conflicting key carried in the <c>TagConfigJson</c> base.
    /// </summary>
    /// <param name="baseJson">The base TagConfig JSON (from the row's <c>TagConfigJson</c> column), or null/blank.</param>
    /// <param name="row">The CSV row as a header→value map.</param>
    /// <returns>The merged TagConfig JSON string.</returns>
    /// <exception cref="FormatException">A typed cell is not a valid value for its column (bad enum / number / bool).</exception>
    public abstract string ApplyTypedColumns(string? baseJson, IReadOnlyDictionary<string, string> row);

    /// <summary>
    /// Projects the typed columns out of a stored TagConfig JSON for export — the inverse of
    /// <see cref="ApplyTypedColumns"/>. A column absent from the JSON is omitted from the result.
    /// </summary>
    /// <param name="tagConfigJson">The tag's stored TagConfig JSON, or null/blank.</param>
    /// <returns>The typed columns present, keyed by CSV header.</returns>
    public abstract IReadOnlyDictionary<string, string> ReadTypedColumns(string? tagConfigJson);
}

/// <summary>One typed column of a model-backed map: its metadata plus the read/write pair over the
/// concrete <typeparamref name="TModel"/> working model.</summary>
/// <typeparam name="TModel">The driver's TagConfig working-model type.</typeparam>
/// <param name="Meta">The column metadata (header / JSON key / model property).</param>
/// <param name="Get">Projects the model's value to a cell string, or <c>null</c> to omit the column on export.</param>
/// <param name="Set">Parses a non-blank cell onto the model (throws <see cref="FormatException"/> on a bad value).</param>
public sealed record CsvModelColumn<TModel>(
    CsvTypedColumn Meta,
    Func<TModel, string?> Get,
    Action<TModel, string> Set);

/// <summary>
/// A <see cref="CsvDriverTagMap"/> driven by a driver's typed working model. Import loads the base JSON
/// through the model's <c>FromJson</c> (preserving unknown keys), applies each present typed cell via its
/// <see cref="CsvModelColumn{TModel}.Set"/>, and serialises back through the model's <c>ToJson</c> — so
/// the CSV path reuses the exact same preserve-unknown / typed-wins merge the TagModal editor uses.
/// </summary>
/// <typeparam name="TModel">The driver's TagConfig working-model type.</typeparam>
public sealed class ModelBackedCsvDriverTagMap<TModel> : CsvDriverTagMap
{
    private readonly Func<string?, TModel> _fromJson;
    private readonly Func<TModel, string> _toJson;
    private readonly IReadOnlyList<CsvModelColumn<TModel>> _columns;

    /// <summary>Builds a model-backed map.</summary>
    /// <param name="driverType">The driver-type string served.</param>
    /// <param name="fromJson">The model's <c>FromJson</c> loader (preserves unknown keys).</param>
    /// <param name="toJson">The model's <c>ToJson</c> serialiser.</param>
    /// <param name="columns">The typed columns (metadata + read/write pairs).</param>
    public ModelBackedCsvDriverTagMap(
        string driverType,
        Func<string?, TModel> fromJson,
        Func<TModel, string> toJson,
        IReadOnlyList<CsvModelColumn<TModel>> columns)
    {
        DriverType = driverType;
        _fromJson = fromJson;
        _toJson = toJson;
        _columns = columns;
        Columns = columns.Select(c => c.Meta).ToArray();
    }

    /// <inheritdoc/>
    public override string DriverType { get; }

    /// <inheritdoc/>
    public override Type? ModelType => typeof(TModel);

    /// <inheritdoc/>
    public override IReadOnlyList<CsvTypedColumn> Columns { get; }

    /// <inheritdoc/>
    public override string ApplyTypedColumns(string? baseJson, IReadOnlyDictionary<string, string> row)
    {
        var model = _fromJson(baseJson);
        foreach (var col in _columns)
        {
            if (row.TryGetValue(col.Meta.Header, out var raw) && !string.IsNullOrWhiteSpace(raw))
            {
                col.Set(model, raw.Trim());
            }
        }

        return _toJson(model);
    }

    /// <inheritdoc/>
    public override IReadOnlyDictionary<string, string> ReadTypedColumns(string? tagConfigJson)
    {
        var model = _fromJson(tagConfigJson);
        var result = new Dictionary<string, string>(StringComparer.Ordinal);
        foreach (var col in _columns)
        {
            if (col.Get(model) is { } v)
            {
                result[col.Meta.Header] = v;
            }
        }

        return result;
    }
}

/// <summary>The value shape of a raw-key (model-less) CSV column.</summary>
public enum CsvRawKeyKind
{
    /// <summary>A JSON string value.</summary>
    String,

    /// <summary>A JSON boolean value (parsed case-insensitively from <c>true</c>/<c>false</c>).</summary>
    Bool,

    /// <summary>A JSON integer value.</summary>
    Int,
}

/// <summary>Metadata + value-kind for one column of a raw-key map.</summary>
/// <param name="Meta">The column metadata (header / JSON key; model property is null).</param>
/// <param name="Kind">The JSON value kind the header maps onto.</param>
public sealed record CsvRawKeyColumn(CsvTypedColumn Meta, CsvRawKeyKind Kind);

/// <summary>
/// A <see cref="CsvDriverTagMap"/> for drivers with no typed working model (Galaxy, Calculation): each
/// column maps a CSV header directly onto a root TagConfig JSON key, preserving every other key across
/// the round-trip.
/// </summary>
public sealed class RawKeyCsvDriverTagMap : CsvDriverTagMap
{
    private readonly IReadOnlyList<CsvRawKeyColumn> _columns;

    /// <summary>Builds a raw-key map.</summary>
    /// <param name="driverType">The driver-type string served.</param>
    /// <param name="columns">The header→JSON-key columns with their value kinds.</param>
    public RawKeyCsvDriverTagMap(string driverType, IReadOnlyList<CsvRawKeyColumn> columns)
    {
        DriverType = driverType;
        _columns = columns;
        Columns = columns.Select(c => c.Meta).ToArray();
    }

    /// <inheritdoc/>
    public override string DriverType { get; }

    /// <inheritdoc/>
    public override Type? ModelType => null;

    /// <inheritdoc/>
    public override IReadOnlyList<CsvTypedColumn> Columns { get; }

    /// <inheritdoc/>
    public override string ApplyTypedColumns(string? baseJson, IReadOnlyDictionary<string, string> row)
    {
        var o = TagConfigJson.ParseOrNew(baseJson);
        foreach (var col in _columns)
        {
            if (row.TryGetValue(col.Meta.Header, out var raw) && !string.IsNullOrWhiteSpace(raw))
            {
                var trimmed = raw.Trim();
                o[col.Meta.JsonKey] = col.Kind switch
                {
                    CsvRawKeyKind.Bool => JsonValue.Create(CsvValues.ParseBool(col.Meta.Header, trimmed)),
                    CsvRawKeyKind.Int => JsonValue.Create(CsvValues.ParseInt(col.Meta.Header, trimmed)),
                    _ => JsonValue.Create(trimmed),
                };
            }
        }

        return TagConfigJson.Serialize(o);
    }

    /// <inheritdoc/>
    public override IReadOnlyDictionary<string, string> ReadTypedColumns(string? tagConfigJson)
    {
        var o = TagConfigJson.ParseOrNew(tagConfigJson);
        var result = new Dictionary<string, string>(StringComparer.Ordinal);
        foreach (var col in _columns)
        {
            if (o.TryGetPropertyValue(col.Meta.JsonKey, out var node) && node is JsonValue v)
            {
                result[col.Meta.Header] = col.Kind switch
                {
                    CsvRawKeyKind.Bool => v.TryGetValue<bool>(out var b) ? (b ? "true" : "false") : v.ToString(),
                    CsvRawKeyKind.Int => v.TryGetValue<long>(out var l)
                        ? l.ToString(CultureInfo.InvariantCulture)
                        : v.ToString(),
                    _ => v.TryGetValue<string>(out var s) ? s : v.ToString(),
                };
            }
        }

        return result;
    }
}

/// <summary>Case-insensitive value parsers/formatters for typed CSV cells, throwing a uniform
/// <see cref="FormatException"/> (naming the column + the offending value) the review grid surfaces.</summary>
public static class CsvValues
{
    /// <summary>Parses an enum member name case-insensitively; throws with the legal members listed.</summary>
    /// <typeparam name="TEnum">The enum type.</typeparam>
    /// <param name="header">The CSV column header (for the error message).</param>
    /// <param name="value">The cell value.</param>
    /// <returns>The parsed enum value.</returns>
    /// <exception cref="FormatException">The value is not a member of <typeparamref name="TEnum"/>.</exception>
    public static TEnum ParseEnum<TEnum>(string header, string value) where TEnum : struct, Enum
        => Enum.TryParse<TEnum>(value, ignoreCase: true, out var v) && Enum.IsDefined(v)
            ? v
            : throw new FormatException(
                $"Column '{header}': '{value}' is not a valid {typeof(TEnum).Name} " +
                $"(expected one of: {string.Join(", ", Enum.GetNames<TEnum>())}).");

    /// <summary>Parses an integer (invariant); throws a column-named <see cref="FormatException"/>.</summary>
    /// <param name="header">The CSV column header (for the error message).</param>
    /// <param name="value">The cell value.</param>
    /// <returns>The parsed integer.</returns>
    /// <exception cref="FormatException">The value is not an integer.</exception>
    public static int ParseInt(string header, string value)
        => int.TryParse(value, NumberStyles.Integer, CultureInfo.InvariantCulture, out var i)
            ? i
            : throw new FormatException($"Column '{header}': '{value}' is not a whole number.");

    /// <summary>Parses a boolean case-insensitively (<c>true</c>/<c>false</c>); throws a column-named error.</summary>
    /// <param name="header">The CSV column header (for the error message).</param>
    /// <param name="value">The cell value.</param>
    /// <returns>The parsed boolean.</returns>
    /// <exception cref="FormatException">The value is not a boolean.</exception>
    public static bool ParseBool(string header, string value)
        => bool.TryParse(value, out var b)
            ? b
            : throw new FormatException($"Column '{header}': '{value}' is not a boolean (expected true or false).");

    /// <summary>Formats a nullable boolean for export: <c>null</c> ⇒ omitted (null cell), else lowercased.</summary>
    /// <param name="value">The nullable boolean.</param>
    /// <returns>The cell string, or <c>null</c> to omit the column.</returns>
    public static string? FormatNullableBool(bool? value) => value is { } b ? (b ? "true" : "false") : null;
}

/// <summary>
/// Registry of the WP5 CSV column dictionary: the common columns every device's export/import carries,
/// plus the per-driver typed <see cref="CsvDriverTagMap"/> resolved by driver type. The single authority
/// for "which columns does a device of driver X have".
/// </summary>
public static class CsvColumnMap
{
    /// <summary>The <c>TagConfigJson</c> fallback column header — the base under which typed columns merge.</summary>
    public const string TagConfigJsonColumn = "TagConfigJson";

    /// <summary>The common column headers that precede any typed driver columns (in order).</summary>
    public static IReadOnlyList<string> CommonLeadingColumns { get; } = new[]
    {
        "Name",
        "TagGroupPath",
        "DataType",
        "AccessLevel",
        "WriteIdempotent",
        "PollGroup",
        "IsHistorized",
        "HistorianTagname",
        "IsArray",
        "ArrayLength",
        "Alarm.AlarmType",
        "Alarm.Severity",
        "Alarm.HistorizeToAveva",
    };

    /// <summary>All common columns (leading columns + the trailing <c>TagConfigJson</c> fallback).</summary>
    public static IReadOnlyList<string> CommonColumns { get; } =
        [.. CommonLeadingColumns, TagConfigJsonColumn];

    /// <summary>The Calculation (virtual-tag) driver-type string.</summary>
    /// <remarks>Retained as an alias for <see cref="DriverTypeNames.Calculation"/>, which now exists — the
    /// original note here ("not yet a DriverTypeNames constant; its factory lands in a later Batch-2
    /// package") went stale once the factory registered.</remarks>
    public const string CalculationDriverType = DriverTypeNames.Calculation;

    private static readonly IReadOnlyDictionary<string, CsvDriverTagMap> Maps = BuildMaps();

    /// <summary>All registered driver maps.</summary>
    public static IReadOnlyCollection<CsvDriverTagMap> All => (IReadOnlyCollection<CsvDriverTagMap>)Maps.Values;

    /// <summary>Resolves the typed map for a driver type, or <c>null</c> when the driver has no typed
    /// columns (only the common set applies).</summary>
    /// <param name="driverType">The driver-type string.</param>
    /// <returns>The map, or <c>null</c>.</returns>
    public static CsvDriverTagMap? Resolve(string? driverType)
        => driverType is not null && Maps.TryGetValue(driverType, out var m) ? m : null;

    /// <summary>The full ordered header row for a device of the given driver type: the leading common
    /// columns, then the driver's typed columns, then the trailing <c>TagConfigJson</c> fallback.</summary>
    /// <param name="driverType">The device's driver-type string.</param>
    /// <returns>The ordered CSV header names.</returns>
    public static IReadOnlyList<string> HeadersFor(string? driverType)
    {
        var typed = Resolve(driverType)?.Headers ?? Array.Empty<string>();
        return [.. CommonLeadingColumns, .. typed, TagConfigJsonColumn];
    }

    private static Dictionary<string, CsvDriverTagMap> BuildMaps()
    {
        var maps = new CsvDriverTagMap[]
        {
            new ModelBackedCsvDriverTagMap<ModbusTagConfigModel>(
                DriverTypeNames.Modbus, ModbusTagConfigModel.FromJson, m => m.ToJson(),
                new[]
                {
                    ModbusCol("Region", "region", nameof(ModbusTagConfigModel.Region),
                        m => m.Region.ToString(), (m, s) => m.Region = CsvValues.ParseEnum<ModbusRegion>("Region", s)),
                    ModbusCol("Address", "address", nameof(ModbusTagConfigModel.Address),
                        m => m.Address.ToString(CultureInfo.InvariantCulture), (m, s) => m.Address = CsvValues.ParseInt("Address", s)),
                    ModbusCol("ModbusDataType", "dataType", nameof(ModbusTagConfigModel.DataType),
                        m => m.DataType.ToString(), (m, s) => m.DataType = CsvValues.ParseEnum<ModbusDataType>("ModbusDataType", s)),
                    ModbusCol("ByteOrder", "byteOrder", nameof(ModbusTagConfigModel.ByteOrder),
                        m => m.ByteOrder.ToString(), (m, s) => m.ByteOrder = CsvValues.ParseEnum<ModbusByteOrder>("ByteOrder", s)),
                    ModbusCol("BitIndex", "bitIndex", nameof(ModbusTagConfigModel.BitIndex),
                        m => m.BitIndex.ToString(CultureInfo.InvariantCulture), (m, s) => m.BitIndex = CsvValues.ParseInt("BitIndex", s)),
                    ModbusCol("StringLength", "stringLength", nameof(ModbusTagConfigModel.StringLength),
                        m => m.StringLength.ToString(CultureInfo.InvariantCulture), (m, s) => m.StringLength = CsvValues.ParseInt("StringLength", s)),
                    ModbusCol("Writable", "writable", nameof(ModbusTagConfigModel.Writable),
                        m => CsvValues.FormatNullableBool(m.Writable), (m, s) => m.Writable = CsvValues.ParseBool("Writable", s)),
                }),

            new ModelBackedCsvDriverTagMap<S7TagConfigModel>(
                DriverTypeNames.S7, S7TagConfigModel.FromJson, m => m.ToJson(),
                new[]
                {
                    S7Col("Address", "address", nameof(S7TagConfigModel.Address),
                        m => m.Address, (m, s) => m.Address = s),
                    S7Col("S7DataType", "dataType", nameof(S7TagConfigModel.DataType),
                        m => m.DataType.ToString(), (m, s) => m.DataType = CsvValues.ParseEnum<S7DataType>("S7DataType", s)),
                    S7Col("StringLength", "stringLength", nameof(S7TagConfigModel.StringLength),
                        m => m.StringLength.ToString(CultureInfo.InvariantCulture), (m, s) => m.StringLength = CsvValues.ParseInt("StringLength", s)),
                    S7Col("Writable", "writable", nameof(S7TagConfigModel.Writable),
                        m => CsvValues.FormatNullableBool(m.Writable), (m, s) => m.Writable = CsvValues.ParseBool("Writable", s)),
                }),

            new ModelBackedCsvDriverTagMap<AbCipTagConfigModel>(
                DriverTypeNames.AbCip, AbCipTagConfigModel.FromJson, m => m.ToJson(),
                new[]
                {
                    AbCipCol("TagPath", "tagPath", nameof(AbCipTagConfigModel.TagPath),
                        m => m.TagPath, (m, s) => m.TagPath = s),
                    AbCipCol("AbCipDataType", "dataType", nameof(AbCipTagConfigModel.DataType),
                        m => m.DataType.ToString(), (m, s) => m.DataType = CsvValues.ParseEnum<AbCipDataType>("AbCipDataType", s)),
                    AbCipCol("Writable", "writable", nameof(AbCipTagConfigModel.Writable),
                        m => CsvValues.FormatNullableBool(m.Writable), (m, s) => m.Writable = CsvValues.ParseBool("Writable", s)),
                }),

            new ModelBackedCsvDriverTagMap<AbLegacyTagConfigModel>(
                DriverTypeNames.AbLegacy, AbLegacyTagConfigModel.FromJson, m => m.ToJson(),
                new[]
                {
                    AbLegacyCol("Address", "address", nameof(AbLegacyTagConfigModel.Address),
                        m => m.Address, (m, s) => m.Address = s),
                    AbLegacyCol("AbLegacyDataType", "dataType", nameof(AbLegacyTagConfigModel.DataType),
                        m => m.DataType.ToString(), (m, s) => m.DataType = CsvValues.ParseEnum<AbLegacyDataType>("AbLegacyDataType", s)),
                    AbLegacyCol("Writable", "writable", nameof(AbLegacyTagConfigModel.Writable),
                        m => CsvValues.FormatNullableBool(m.Writable), (m, s) => m.Writable = CsvValues.ParseBool("Writable", s)),
                }),

            new ModelBackedCsvDriverTagMap<TwinCATTagConfigModel>(
                DriverTypeNames.TwinCAT, TwinCATTagConfigModel.FromJson, m => m.ToJson(),
                new[]
                {
                    TwinCatCol("SymbolPath", "symbolPath", nameof(TwinCATTagConfigModel.SymbolPath),
                        m => m.SymbolPath, (m, s) => m.SymbolPath = s),
                    TwinCatCol("TwinCATDataType", "dataType", nameof(TwinCATTagConfigModel.DataType),
                        m => m.DataType.ToString(), (m, s) => m.DataType = CsvValues.ParseEnum<TwinCATDataType>("TwinCATDataType", s)),
                    TwinCatCol("Writable", "writable", nameof(TwinCATTagConfigModel.Writable),
                        m => CsvValues.FormatNullableBool(m.Writable), (m, s) => m.Writable = CsvValues.ParseBool("Writable", s)),
                }),

            new ModelBackedCsvDriverTagMap<FocasTagConfigModel>(
                DriverTypeNames.FOCAS, FocasTagConfigModel.FromJson, m => m.ToJson(),
                new[]
                {
                    FocasCol("Address", "address", nameof(FocasTagConfigModel.Address),
                        m => m.Address, (m, s) => m.Address = s),
                    FocasCol("FocasDataType", "dataType", nameof(FocasTagConfigModel.DataType),
                        m => m.DataType.ToString(), (m, s) => m.DataType = CsvValues.ParseEnum<FocasDataType>("FocasDataType", s)),
                }),

            new ModelBackedCsvDriverTagMap<OpcUaClientTagConfigModel>(
                DriverTypeNames.OpcUaClient, OpcUaClientTagConfigModel.FromJson, m => m.ToJson(),
                new[]
                {
                    OpcUaCol("NodeId", "nodeId", nameof(OpcUaClientTagConfigModel.NodeId),
                        m => m.NodeId, (m, s) => m.NodeId = s),
                }),

            new RawKeyCsvDriverTagMap(DriverTypeNames.Galaxy, new[]
            {
                // Galaxy's driver reference is the PascalCase FullName key (tag_name.AttributeName).
                new CsvRawKeyColumn(new CsvTypedColumn("AttributeRef", "FullName", null), CsvRawKeyKind.String),
            }),

            new RawKeyCsvDriverTagMap(CalculationDriverType, new[]
            {
                new CsvRawKeyColumn(new CsvTypedColumn("ScriptId", "scriptId", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("ChangeTriggered", "changeTriggered", null), CsvRawKeyKind.Bool),
                new CsvRawKeyColumn(new CsvTypedColumn("TimerIntervalMs", "timerIntervalMs", null), CsvRawKeyKind.Int),
            }),

            // deferment.md G-5 — Sql, Mqtt and MTConnect all have typed tag editors but had NO CSV map, so
            // import/export silently degraded them to the raw TagConfigJson fallback while the seven older
            // drivers got typed columns. Raw-key maps (as Galaxy and Calculation use) rather than
            // model-backed ones: they address the identity keys directly, which is what a CSV round-trip
            // needs, without duplicating each model's full accessor surface.
            new RawKeyCsvDriverTagMap(DriverTypeNames.Sql, new[]
            {
                new CsvRawKeyColumn(new CsvTypedColumn("Model", "model", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("Table", "table", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("KeyColumn", "keyColumn", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("KeyValue", "keyValue", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("ValueColumn", "valueColumn", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("ColumnName", "columnName", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("SqlType", "type", null), CsvRawKeyKind.String),
            }),

            new RawKeyCsvDriverTagMap(DriverTypeNames.Mqtt, new[]
            {
                new CsvRawKeyColumn(new CsvTypedColumn("Mode", "mode", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("Topic", "topic", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("PayloadFormat", "payloadFormat", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("JsonPath", "jsonPath", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("MqttDataType", "dataType", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("GroupId", "groupId", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("EdgeNodeId", "edgeNodeId", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("MqttDeviceId", "deviceId", null), CsvRawKeyKind.String),
            }),

            new RawKeyCsvDriverTagMap(DriverTypeNames.MTConnect, new[]
            {
                new CsvRawKeyColumn(new CsvTypedColumn("DataItemId", "fullName", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("MtDataType", "dataType", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("MtCategory", "mtCategory", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("MtType", "mtType", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("MtSubType", "mtSubType", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("MtUnits", "units", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("MtDevice", "mtDevice", null), CsvRawKeyKind.String),
                new CsvRawKeyColumn(new CsvTypedColumn("MtComponent", "mtComponent", null), CsvRawKeyKind.String),
            }),
        };

        return maps.ToDictionary(m => m.DriverType, m => m, StringComparer.OrdinalIgnoreCase);
    }

    // Per-driver column factories — one per model so the Get/Set lambdas stay strongly typed.
    private static CsvModelColumn<ModbusTagConfigModel> ModbusCol(
        string h, string k, string p, Func<ModbusTagConfigModel, string?> get, Action<ModbusTagConfigModel, string> set)
        => new(new CsvTypedColumn(h, k, p), get, set);

    private static CsvModelColumn<S7TagConfigModel> S7Col(
        string h, string k, string p, Func<S7TagConfigModel, string?> get, Action<S7TagConfigModel, string> set)
        => new(new CsvTypedColumn(h, k, p), get, set);

    private static CsvModelColumn<AbCipTagConfigModel> AbCipCol(
        string h, string k, string p, Func<AbCipTagConfigModel, string?> get, Action<AbCipTagConfigModel, string> set)
        => new(new CsvTypedColumn(h, k, p), get, set);

    private static CsvModelColumn<AbLegacyTagConfigModel> AbLegacyCol(
        string h, string k, string p, Func<AbLegacyTagConfigModel, string?> get, Action<AbLegacyTagConfigModel, string> set)
        => new(new CsvTypedColumn(h, k, p), get, set);

    private static CsvModelColumn<TwinCATTagConfigModel> TwinCatCol(
        string h, string k, string p, Func<TwinCATTagConfigModel, string?> get, Action<TwinCATTagConfigModel, string> set)
        => new(new CsvTypedColumn(h, k, p), get, set);

    private static CsvModelColumn<FocasTagConfigModel> FocasCol(
        string h, string k, string p, Func<FocasTagConfigModel, string?> get, Action<FocasTagConfigModel, string> set)
        => new(new CsvTypedColumn(h, k, p), get, set);

    private static CsvModelColumn<OpcUaClientTagConfigModel> OpcUaCol(
        string h, string k, string p, Func<OpcUaClientTagConfigModel, string?> get, Action<OpcUaClientTagConfigModel, string> set)
        => new(new CsvTypedColumn(h, k, p), get, set);
}
