using System.Text.Json;
using ZB.MOM.WW.OtOpcUa.OpcUaServer;

namespace ZB.MOM.WW.OtOpcUa.Runtime.Drivers;

/// <summary>
///     Minimal driver-side view of the deployment artifact emitted by
///     <c>ConfigComposer.SnapshotAndFlattenAsync</c>. The artifact JSON is the full snapshot —
///     for driver spawning we only need the <c>DriverInstances</c> array. Reading just the
///     subset keeps allocations cheap on every deploy.
/// </summary>
public sealed record DriverInstanceSpec(
    Guid DriverInstanceRowId,
    string DriverInstanceId,
    string Name,
    string DriverType,
    bool Enabled,
    string DriverConfig,
    string? ClusterId = null);

public static class DeploymentArtifact
{
    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        PropertyNameCaseInsensitive = true,
    };

    /// <summary>
    ///     Parse a deployment artifact blob into the list of driver-instance specs to spawn.
    ///     Empty / malformed blobs return an empty list — callers log + treat as "no drivers".
    /// </summary>
    /// <param name="blob">The deployment artifact blob to parse.</param>
    public static IReadOnlyList<DriverInstanceSpec> ParseDriverInstances(ReadOnlySpan<byte> blob)
    {
        if (blob.IsEmpty) return Array.Empty<DriverInstanceSpec>();

        try
        {
            using var doc = JsonDocument.Parse(blob.ToArray());
            if (!doc.RootElement.TryGetProperty("DriverInstances", out var arr)
                || arr.ValueKind != JsonValueKind.Array)
            {
                return Array.Empty<DriverInstanceSpec>();
            }

            var result = new List<DriverInstanceSpec>(arr.GetArrayLength());
            foreach (var el in arr.EnumerateArray())
            {
                if (el.ValueKind != JsonValueKind.Object) continue;
                var spec = TryReadSpec(el);
                if (spec is not null) result.Add(spec);
            }
            return result;
        }
        catch (JsonException)
        {
            return Array.Empty<DriverInstanceSpec>();
        }
    }

    private static DriverInstanceSpec? TryReadSpec(JsonElement el)
    {
        var rowId = el.TryGetProperty("DriverInstanceRowId", out var rowEl)
            && rowEl.TryGetGuid(out var rid) ? rid : Guid.Empty;
        var id = el.TryGetProperty("DriverInstanceId", out var idEl) ? idEl.GetString() : null;
        var name = el.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() : null;
        var type = el.TryGetProperty("DriverType", out var typeEl) ? typeEl.GetString() : null;
        var enabled = !el.TryGetProperty("Enabled", out var enEl) || enEl.GetBoolean();
        var config = el.TryGetProperty("DriverConfig", out var cfgEl) ? cfgEl.GetString() : null;
        var clusterId = el.TryGetProperty("ClusterId", out var clEl) ? clEl.GetString() : null;

        if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(type)) return null;

        return new DriverInstanceSpec(
            DriverInstanceRowId: rowId,
            DriverInstanceId: id!,
            Name: name ?? id!,
            DriverType: type!,
            Enabled: enabled,
            DriverConfig: config ?? "{}",
            ClusterId: clusterId);
    }

    /// <summary>
    ///     Parse the artifact into the projected <see cref="Phase7CompositionResult"/> used by
    ///     <c>Phase7Planner</c> + <c>Phase7Applier</c>. Returns an empty composition for empty/
    ///     malformed blobs so callers can treat parse failure as a no-op deploy.
    ///
    ///     The artifact JSON is produced by <c>ConfigComposer.SnapshotAndFlattenAsync</c> in the
    ///     ControlPlane — its Pascal-case property names match the EF entities. We only need a
    ///     subset of fields per entity class to drive the address-space rebuild on driver-role
    ///     nodes.
    /// </summary>
    /// <param name="blob">The deployment artifact blob to parse.</param>
    public static Phase7CompositionResult ParseComposition(ReadOnlySpan<byte> blob)
    {
        if (blob.IsEmpty) return Empty();

        try
        {
            using var doc = JsonDocument.Parse(blob.ToArray());
            var root = doc.RootElement;

            var areas = ReadArray(root, "UnsAreas", ReadAreaProjection);
            var lines = ReadArray(root, "UnsLines", ReadLineProjection);
            var equipment = ReadArray(root, "Equipment", ReadEquipmentNode);
            var drivers = ReadArray(root, "DriverInstances", ReadDriverPlan);
            var alarms = ReadArray(root, "ScriptedAlarms", ReadAlarmPlan);
            var galaxyTags = BuildGalaxyTagPlans(root, drivers);

            return new Phase7CompositionResult(areas, lines, equipment, drivers, alarms, galaxyTags);
        }
        catch (JsonException)
        {
            return Empty();
        }
    }

    private static Phase7CompositionResult Empty() => new(
        Array.Empty<UnsAreaProjection>(),
        Array.Empty<UnsLineProjection>(),
        Array.Empty<EquipmentNode>(),
        Array.Empty<DriverInstancePlan>(),
        Array.Empty<ScriptedAlarmPlan>(),
        Array.Empty<GalaxyTagPlan>());

    /// <summary>
    ///     Cross-reference the artifact's Tags + Namespaces + DriverInstances arrays to find
    ///     SystemPlatform-namespace tags (Galaxy hierarchy), then emit one <see cref="GalaxyTagPlan"/>
    ///     per qualifying tag. Mirrors <c>Phase7Composer.Compose</c>'s filter so a compose-side
    ///     plan and an artifact-decode plan agree on the same set of tags.
    /// </summary>
    private static IReadOnlyList<GalaxyTagPlan> BuildGalaxyTagPlans(JsonElement root, IReadOnlyList<DriverInstancePlan> drivers)
    {
        if (!root.TryGetProperty("Tags", out var tagsArr) || tagsArr.ValueKind != JsonValueKind.Array)
            return Array.Empty<GalaxyTagPlan>();
        if (!root.TryGetProperty("Namespaces", out var nsArr) || nsArr.ValueKind != JsonValueKind.Array)
            return Array.Empty<GalaxyTagPlan>();
        if (!root.TryGetProperty("DriverInstances", out var diArr) || diArr.ValueKind != JsonValueKind.Array)
            return Array.Empty<GalaxyTagPlan>();

        // namespaceId → Kind ("SystemPlatform"/"Equipment"/"Simulated") — enum serialises as int by default,
        // but ConfigComposer's snapshot uses default JsonSerializer which writes numbers. Tolerate both.
        var systemPlatformNamespaces = new HashSet<string>(StringComparer.Ordinal);
        foreach (var el in nsArr.EnumerateArray())
        {
            if (el.ValueKind != JsonValueKind.Object) continue;
            var id = el.TryGetProperty("NamespaceId", out var idEl) ? idEl.GetString() : null;
            if (string.IsNullOrWhiteSpace(id)) continue;
            if (!el.TryGetProperty("Kind", out var kindEl)) continue;
            var isSystemPlatform = kindEl.ValueKind switch
            {
                JsonValueKind.Number => kindEl.GetInt32() == 1, // NamespaceKind.SystemPlatform = 1
                JsonValueKind.String => string.Equals(kindEl.GetString(), "SystemPlatform", StringComparison.Ordinal),
                _ => false,
            };
            if (isSystemPlatform) systemPlatformNamespaces.Add(id!);
        }

        // driverInstanceId → namespaceId
        var driverToNamespace = new Dictionary<string, string>(StringComparer.Ordinal);
        foreach (var el in diArr.EnumerateArray())
        {
            if (el.ValueKind != JsonValueKind.Object) continue;
            var id = el.TryGetProperty("DriverInstanceId", out var idEl) ? idEl.GetString() : null;
            var ns = el.TryGetProperty("NamespaceId", out var nsEl) ? nsEl.GetString() : null;
            if (!string.IsNullOrWhiteSpace(id) && !string.IsNullOrWhiteSpace(ns))
                driverToNamespace[id!] = ns!;
        }

        var result = new List<GalaxyTagPlan>(tagsArr.GetArrayLength());
        foreach (var el in tagsArr.EnumerateArray())
        {
            if (el.ValueKind != JsonValueKind.Object) continue;
            // Skip tags with non-null EquipmentId (Equipment-namespace tags belong to a different path).
            if (el.TryGetProperty("EquipmentId", out var eqEl) && eqEl.ValueKind != JsonValueKind.Null) continue;

            var tagId = el.TryGetProperty("TagId", out var tEl) ? tEl.GetString() : null;
            var di = el.TryGetProperty("DriverInstanceId", out var diEl) ? diEl.GetString() : null;
            var name = el.TryGetProperty("Name", out var nmEl) ? nmEl.GetString() : null;
            var folder = el.TryGetProperty("FolderPath", out var fpEl) && fpEl.ValueKind != JsonValueKind.Null
                ? fpEl.GetString() : null;
            var dataType = el.TryGetProperty("DataType", out var dtEl) ? dtEl.GetString() : null;

            if (string.IsNullOrWhiteSpace(tagId) || string.IsNullOrWhiteSpace(di) || string.IsNullOrWhiteSpace(name))
                continue;
            if (!driverToNamespace.TryGetValue(di!, out var nsId)) continue;
            if (!systemPlatformNamespaces.Contains(nsId)) continue;

            var folderPath = folder ?? string.Empty;
            var mxRef = string.IsNullOrWhiteSpace(folderPath) ? name! : $"{folderPath}.{name}";
            result.Add(new GalaxyTagPlan(tagId!, di!, folderPath, name!, dataType ?? "BaseDataType", mxRef));
        }

        result.Sort((a, b) =>
        {
            var byDriver = string.CompareOrdinal(a.DriverInstanceId, b.DriverInstanceId);
            if (byDriver != 0) return byDriver;
            var byFolder = string.CompareOrdinal(a.FolderPath, b.FolderPath);
            if (byFolder != 0) return byFolder;
            return string.CompareOrdinal(a.DisplayName, b.DisplayName);
        });
        return result;
    }

    private static IReadOnlyList<T> ReadArray<T>(JsonElement root, string propertyName, Func<JsonElement, T?> reader)
        where T : class
    {
        if (!root.TryGetProperty(propertyName, out var arr) || arr.ValueKind != JsonValueKind.Array)
            return Array.Empty<T>();

        var result = new List<T>(arr.GetArrayLength());
        foreach (var el in arr.EnumerateArray())
        {
            if (el.ValueKind != JsonValueKind.Object) continue;
            var item = reader(el);
            if (item is not null) result.Add(item);
        }
        // Match Phase7Composer's natural-key sort so plan diffs are deterministic across
        // artifact-decode + composer-compose passes.
        return result.OrderBy(IdentityOf, StringComparer.Ordinal).ToList();
    }

    private static string IdentityOf<T>(T item) where T : class => item switch
    {
        UnsAreaProjection a => a.UnsAreaId,
        UnsLineProjection l => l.UnsLineId,
        EquipmentNode e => e.EquipmentId,
        DriverInstancePlan d => d.DriverInstanceId,
        ScriptedAlarmPlan a => a.ScriptedAlarmId,
        _ => string.Empty,
    };

    private static UnsAreaProjection? ReadAreaProjection(JsonElement el)
    {
        var id = el.TryGetProperty("UnsAreaId", out var idEl) ? idEl.GetString() : null;
        var name = el.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() : null;
        if (string.IsNullOrWhiteSpace(id)) return null;
        return new UnsAreaProjection(id!, name ?? id!);
    }

    private static UnsLineProjection? ReadLineProjection(JsonElement el)
    {
        var id = el.TryGetProperty("UnsLineId", out var idEl) ? idEl.GetString() : null;
        var areaId = el.TryGetProperty("UnsAreaId", out var areaEl) ? areaEl.GetString() : null;
        var name = el.TryGetProperty("Name", out var nameEl) ? nameEl.GetString() : null;
        if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(areaId)) return null;
        return new UnsLineProjection(id!, areaId!, name ?? id!);
    }

    private static EquipmentNode? ReadEquipmentNode(JsonElement el)
    {
        var id = el.TryGetProperty("EquipmentId", out var idEl) ? idEl.GetString() : null;
        var displayName = el.TryGetProperty("MachineCode", out var mcEl) ? mcEl.GetString() : null;
        var lineId = el.TryGetProperty("UnsLineId", out var lineEl) ? lineEl.GetString() : null;
        if (string.IsNullOrWhiteSpace(id)) return null;
        return new EquipmentNode(id!, displayName ?? id!, lineId ?? string.Empty);
    }

    private static DriverInstancePlan? ReadDriverPlan(JsonElement el)
    {
        var id = el.TryGetProperty("DriverInstanceId", out var idEl) ? idEl.GetString() : null;
        var type = el.TryGetProperty("DriverType", out var typeEl) ? typeEl.GetString() : null;
        var config = el.TryGetProperty("DriverConfig", out var cfgEl) ? cfgEl.GetString() : null;
        if (string.IsNullOrWhiteSpace(id) || string.IsNullOrWhiteSpace(type)) return null;
        return new DriverInstancePlan(id!, type!, config ?? "{}");
    }

    private static ScriptedAlarmPlan? ReadAlarmPlan(JsonElement el)
    {
        var id = el.TryGetProperty("ScriptedAlarmId", out var idEl) ? idEl.GetString() : null;
        var equipmentId = el.TryGetProperty("EquipmentId", out var eqEl) ? eqEl.GetString() : null;
        var script = el.TryGetProperty("PredicateScriptId", out var scEl) ? scEl.GetString() : null;
        var template = el.TryGetProperty("MessageTemplate", out var tmEl) ? tmEl.GetString() : null;
        if (string.IsNullOrWhiteSpace(id)) return null;
        return new ScriptedAlarmPlan(id!, equipmentId ?? string.Empty, script ?? string.Empty, template ?? string.Empty);
    }
}
