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

namespace ZB.MOM.WW.OtOpcUa.Commons.Types;

/// <summary>
///     v3 endpoint→<c>DeviceConfig</c> merge authority. Folds a driver's <c>DriverConfig</c> JSON
///     (protocol/channel-level settings) together with its per-<c>Device</c> <c>DeviceConfig</c> JSON
///     (host/endpoint + per-device settings — the Kepware channel/device split) plus the driver's authored
///     raw tags into the SINGLE config blob every driver factory binds from. Shared by the deploy-decode
///     seam (<c>DeploymentArtifact</c>, which builds each <c>DriverInstanceSpec.DriverConfig</c>) and — in
///     Batch 2 — the browse modal (which needs the same merged shape as the browser's <c>configJson</c>),
///     so the merge lives in exactly one place.
///     <para><b>Contract.</b> Output = the parsed <c>DriverConfig</c> object with, in order:
///     <list type="number">
///       <item>When there is EXACTLY ONE device, its <c>DeviceConfig</c> keys shallow-merged up to the top
///       level (DeviceConfig wins) — so a single-endpoint driver's endpoint (<c>Host</c>/<c>Port</c>,
///       <c>EndpointUrl</c>, gateway address, …) lands where its options bind. Multiple devices do NOT
///       merge up (their endpoints are per-device, read from the array below).</item>
///       <item>A reconstructed <c>Devices</c> array — one object per device = its <c>DeviceConfig</c> keys
///       plus <c>DeviceName</c> = the device's entity <c>Name</c> (the RawPath device segment /
///       <see cref="RawTagEntry.DeviceName"/> routing key). Multi-device drivers read this; single-endpoint
///       drivers ignore the unknown key.</item>
///       <item>A <c>RawTags</c> array = the driver's authored <see cref="RawTagEntry"/> list, so
///       <c>options.RawTags</c> binds.</item>
///     </list>
///     Existing top-level protocol/channel keys survive (single-device DeviceConfig keys override on a name
///     clash — the documented Kepware precedence). Never throws on malformed member JSON: a blank/invalid
///     <c>DriverConfig</c> or <c>DeviceConfig</c> degrades to an empty object for that member.</para>
/// </summary>
public static class DriverDeviceConfigMerger
{
    private static readonly JsonSerializerOptions SerializeOptions = new()
    {
        WriteIndented = false,
        DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.Never,
    };

    /// <summary>The device rows feeding the merge: the entity <c>Name</c> (routing key) + schemaless <c>DeviceConfig</c> JSON.</summary>
    /// <param name="Name">The device's entity <c>Name</c> — the RawPath device segment and multi-device routing key.</param>
    /// <param name="DeviceConfig">The device's schemaless <c>DeviceConfig</c> JSON (endpoint + per-device settings).</param>
    public readonly record struct DeviceRow(string Name, string? DeviceConfig);

    /// <summary>Merge a driver's config + its devices' configs + its authored raw tags into one config blob.</summary>
    /// <param name="driverConfigJson">The driver-level <c>DriverConfig</c> JSON (protocol/channel settings).</param>
    /// <param name="devices">The driver's device rows, in deterministic order (the sole device merges up).</param>
    /// <param name="rawTags">The driver's authored raw tags to inject as the <c>RawTags</c> array.</param>
    /// <returns>The single merged config JSON string the driver factory binds from.</returns>
    public static string Merge(
        string? driverConfigJson,
        IReadOnlyList<DeviceRow> devices,
        IReadOnlyList<RawTagEntry> rawTags)
    {
        ArgumentNullException.ThrowIfNull(devices);
        ArgumentNullException.ThrowIfNull(rawTags);

        var root = ParseObject(driverConfigJson);

        // (1) Exactly one device ⇒ shallow-merge its DeviceConfig up to the top level (DeviceConfig wins),
        // so a single-endpoint driver's endpoint lands where its options bind. Multiple devices are
        // per-device only (read from the Devices array below), so no merge-up.
        if (devices.Count == 1)
        {
            var only = ParseObject(devices[0].DeviceConfig);
            foreach (var (key, value) in only)
                root[key] = value?.DeepClone();
        }

        // (2) Reconstruct the Devices array from ALL device rows: DeviceConfig keys + DeviceName = entity Name.
        var devicesArray = new JsonArray();
        foreach (var device in devices)
        {
            var obj = ParseObject(device.DeviceConfig);
            obj["DeviceName"] = device.Name; // entity Name is the RawPath device segment + routing key
            devicesArray.Add(obj);
        }
        root["Devices"] = devicesArray;

        // (3) Inject the authored raw tags so options.RawTags binds.
        root["RawTags"] = JsonSerializer.SerializeToNode(rawTags, SerializeOptions);

        return root.ToJsonString(SerializeOptions);
    }

    /// <summary>Parse JSON into a mutable <see cref="JsonObject"/>; blank / non-object / malformed ⇒ a fresh empty object.</summary>
    private static JsonObject ParseObject(string? json)
    {
        if (string.IsNullOrWhiteSpace(json)) return new JsonObject();
        try
        {
            return JsonNode.Parse(json) as JsonObject ?? new JsonObject();
        }
        catch (JsonException)
        {
            return new JsonObject();
        }
    }
}
