using System.Text.Json.Nodes;
using ZB.MOM.WW.OtOpcUa.AdminUI.Uns.TagEditors;
using ZB.MOM.WW.OtOpcUa.Commons.Types;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;

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

/// <summary>
/// Pure mapper for the WP6 "Browse device…" re-target: turns a selected driver-browse leaf into a
/// <see cref="RawTagImportRow"/> ready for <see cref="IRawTreeService.ImportTagsAsync"/>. Under the v3
/// identity contract the tag's identity is its RawPath (device + optional group + name), so the leaf's
/// driver reference is written into the driver-typed <c>TagConfig</c> as an ordinary <b>address field</b>
/// (<c>nodeId</c>/<c>tagPath</c>/<c>symbolPath</c>/<c>address</c>/<c>attributeRef</c> per driver) — never an
/// identity key. The address-field write reuses the same <c>&lt;Driver&gt;TagConfigModel</c> the typed tag
/// editors use, so a browse-committed tag round-trips through that editor unchanged.
/// </summary>
public static class RawBrowseCommitMapper
{
    /// <summary>
    /// Maps one selected browse leaf into an import row.
    /// </summary>
    /// <param name="driverType">The owning device's driver type (a <see cref="DriverTypeNames"/> value).</param>
    /// <param name="fullName">The leaf's driver-side full reference (the <c>BrowseNode.NodeId</c> = the
    /// captured <c>DriverAttributeInfo.FullName</c>) — becomes the driver-typed address field.</param>
    /// <param name="browseName">The leaf's browse name — becomes the raw tag <c>Name</c> (a RawPath segment).</param>
    /// <param name="driverDataType">The leaf's <see cref="DriverDataType"/> name (from the browse
    /// attribute side-channel), or null when the browser cannot report a type (e.g. the OPC UA Client tree).</param>
    /// <param name="defaultDataType">The OPC-UA built-in type name to fall back to when
    /// <paramref name="driverDataType"/> is null/unmappable.</param>
    /// <param name="groupPrefix">The target TagGroup's device-relative path when committing under a group,
    /// or null when committing at the device root. Prepended to any mirrored folder path.</param>
    /// <param name="folderPath">The captured browse-folder nesting above the leaf (root→parent display
    /// names); mirrored onto nested TagGroups only when <paramref name="createGroups"/> is true.</param>
    /// <param name="createGroups">When true, mirror <paramref name="folderPath"/> as nested TagGroups.</param>
    /// <returns>The assembled import row.</returns>
    public static RawTagImportRow MapLeaf(
        string driverType,
        string fullName,
        string browseName,
        string? driverDataType,
        string defaultDataType,
        string? groupPrefix,
        IReadOnlyList<string>? folderPath,
        bool createGroups)
    {
        var dataType = MapDataType(driverDataType) ?? defaultDataType;
        var tagConfig = BuildTagConfig(driverType, fullName);
        var mirrored = createGroups && folderPath is { Count: > 0 }
            ? string.Join(RawPaths.Separator, folderPath)
            : null;
        var groupPath = CombineGroupPath(groupPrefix, mirrored);

        // Access baseline mirrors manual entry (Read); write-idempotent stays false (unknown at browse time);
        // no poll-group (batching is authored later).
        return new RawTagImportRow(
            groupPath,
            new RawTagInput(browseName, dataType, TagAccessLevel.Read, WriteIdempotent: false, PollGroupId: null, tagConfig));
    }

    /// <summary>
    /// Maps a <see cref="DriverDataType"/> enum NAME (as reported by the browse attribute side-channel) to the
    /// OPC-UA built-in type name a <see cref="RawTagInput.DataType"/> carries. Mirrors
    /// <c>DiscoveredNodeMapper.ToBuiltinTypeString</c>: most names pass through, <c>Float32</c>/<c>Float64</c>
    /// become <c>Float</c>/<c>Double</c>, and <c>Reference</c> (a Galaxy attribute ref) carries as <c>String</c>.
    /// Returns null when the input is null/blank or not a known driver data type (caller supplies a default).
    /// </summary>
    /// <param name="driverDataType">The <see cref="DriverDataType"/> name, or null/blank.</param>
    /// <returns>The OPC-UA built-in type name, or null when unmappable.</returns>
    public static string? MapDataType(string? driverDataType)
    {
        if (string.IsNullOrWhiteSpace(driverDataType)
            || !Enum.TryParse<DriverDataType>(driverDataType, ignoreCase: true, out var dt))
            return null;

        return dt switch
        {
            DriverDataType.Boolean => "Boolean",
            DriverDataType.Int16 => "Int16",
            DriverDataType.Int32 => "Int32",
            DriverDataType.Int64 => "Int64",
            DriverDataType.UInt16 => "UInt16",
            DriverDataType.UInt32 => "UInt32",
            DriverDataType.UInt64 => "UInt64",
            DriverDataType.Float32 => "Float",
            DriverDataType.Float64 => "Double",
            DriverDataType.String => "String",
            DriverDataType.DateTime => "DateTime",
            DriverDataType.Reference => "String",
            _ => null,
        };
    }

    /// <summary>
    /// Builds the driver-typed <c>TagConfig</c> JSON for a browse-committed tag, setting ONLY the driver's
    /// address field to <paramref name="fullName"/> and leaving every other field at its model default. Reuses
    /// the <c>&lt;Driver&gt;TagConfigModel</c> for driver types that have a typed editor; the model-less Galaxy
    /// driver gets the canonical camelCase <c>attributeRef</c> key directly.
    /// </summary>
    /// <param name="driverType">The owning device's driver type.</param>
    /// <param name="fullName">The leaf's driver-side full reference to write into the address field.</param>
    /// <returns>The serialised driver-typed <c>TagConfig</c> JSON.</returns>
    public static string BuildTagConfig(string driverType, string fullName)
    {
        var address = fullName ?? "";
        if (Is(driverType, DriverTypeNames.OpcUaClient))
            return new OpcUaClientTagConfigModel { NodeId = address }.ToJson();
        if (Is(driverType, DriverTypeNames.AbCip))
            return new AbCipTagConfigModel { TagPath = address }.ToJson();
        if (Is(driverType, DriverTypeNames.AbLegacy))
            return new AbLegacyTagConfigModel { Address = address }.ToJson();
        if (Is(driverType, DriverTypeNames.TwinCAT))
            return new TwinCATTagConfigModel { SymbolPath = address }.ToJson();
        if (Is(driverType, DriverTypeNames.FOCAS))
            return new FocasTagConfigModel { Address = address }.ToJson();
        if (Is(driverType, DriverTypeNames.S7))
            return new S7TagConfigModel { Address = address }.ToJson();
        if (Is(driverType, DriverTypeNames.Galaxy))
            return WriteSingleKey("attributeRef", address);

        // Unknown/flat-address driver (e.g. Modbus is not browsable): record the reference under a generic
        // "address" key so nothing is lost. Browsable drivers are all handled above.
        return WriteSingleKey("address", address);
    }

    /// <summary>
    /// Combines a target-group path prefix with an optional mirrored sub-path, matching the WP5 CSV import
    /// combine semantics: blank collapses to null, and a present prefix + suffix join with the RawPath
    /// separator. Returns null when both are absent (⇒ commit at the device root).
    /// </summary>
    /// <param name="prefix">The target TagGroup's device-relative path, or null/blank for the device root.</param>
    /// <param name="suffix">The mirrored folder sub-path, or null/blank.</param>
    /// <returns>The combined device-relative group path, or null for the device root.</returns>
    public static string? CombineGroupPath(string? prefix, string? suffix)
    {
        prefix = string.IsNullOrWhiteSpace(prefix) ? null : prefix.Trim();
        suffix = string.IsNullOrWhiteSpace(suffix) ? null : suffix.Trim();
        if (prefix is null) return suffix;
        return suffix is null ? prefix : $"{prefix}{RawPaths.SeparatorString}{suffix}";
    }

    private static bool Is(string driverType, string name)
        => string.Equals(driverType, name, StringComparison.OrdinalIgnoreCase);

    private static string WriteSingleKey(string key, string value)
    {
        var o = new JsonObject { [key] = value };
        return o.ToJsonString();
    }
}
