using System.Security.Cryptography;
using System.Text.Json;
using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;

namespace ZB.MOM.WW.OtOpcUa.ControlPlane.AdminOperations;

/// <summary>
/// Pure snapshot composer: reads the current live-edit state from <see cref="OtOpcUaConfigDbContext"/>
/// and serialises it into a deterministic byte[] artifact + SHA-256 hex revision hash. Determinism
/// comes from sorting every collection by its natural key before serialising, so two snapshots over
/// the same DB state always produce the same hash regardless of EF row ordering.
/// </summary>
public static class ConfigComposer
{
    public sealed record ConfigArtifact(byte[] Blob, string RevisionHash);

    private static readonly JsonSerializerOptions JsonOptions = new()
    {
        WriteIndented = false,
        DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.Never,
    };

    /// <summary>Reads the current configuration and returns a deterministic snapshot blob with revision hash.</summary>
    /// <param name="db">The configuration database context.</param>
    /// <param name="ct">The cancellation token for the operation.</param>
    /// <returns>A deterministic <see cref="ConfigArtifact"/> containing the serialised snapshot bytes and its revision hash.</returns>
    public static async Task<ConfigArtifact> SnapshotAndFlattenAsync(
        OtOpcUaConfigDbContext db, CancellationToken ct = default)
    {
        // v3 greenfield snapshot: the Raw tree (RawFolders → DriverInstances(+RawFolderId) → Devices(+DeviceConfig)
        // → TagGroups → Tags(DeviceId/TagGroupId, no EquipmentId/FolderPath/DriverInstanceId)) plus the UNS
        // projection (UnsAreas/UnsLines/Equipment/UnsTagReferences) and the retained VirtualTags/ScriptedAlarms/
        // PollGroups/NodeAcls/Scripts. The Namespaces snapshot is RETIRED (the two OPC UA namespaces are implicit).
        // RevisionHash inputs follow the blob automatically — adding/removing a table shifts the SHA-256.
        var snapshot = new
        {
            Clusters = await db.ServerClusters.AsNoTracking().OrderBy(x => x.ClusterId).ToListAsync(ct),
            Nodes = await db.ClusterNodes.AsNoTracking().OrderBy(x => x.NodeId).ToListAsync(ct),
            RawFolders = await db.RawFolders.AsNoTracking().OrderBy(x => x.RawFolderId).ToListAsync(ct),
            DriverInstances = await db.DriverInstances.AsNoTracking().OrderBy(x => x.DriverInstanceId).ToListAsync(ct),
            Devices = await db.Devices.AsNoTracking().OrderBy(x => x.DeviceId).ToListAsync(ct),
            TagGroups = await db.TagGroups.AsNoTracking().OrderBy(x => x.TagGroupId).ToListAsync(ct),
            Equipment = await db.Equipment.AsNoTracking().OrderBy(x => x.EquipmentId).ToListAsync(ct),
            Tags = await db.Tags.AsNoTracking().OrderBy(x => x.TagId).ToListAsync(ct),
            UnsTagReferences = await db.UnsTagReferences.AsNoTracking().OrderBy(x => x.UnsTagReferenceId).ToListAsync(ct),
            PollGroups = await db.PollGroups.AsNoTracking().OrderBy(x => x.PollGroupId).ToListAsync(ct),
            UnsAreas = await db.UnsAreas.AsNoTracking().OrderBy(x => x.UnsAreaId).ToListAsync(ct),
            UnsLines = await db.UnsLines.AsNoTracking().OrderBy(x => x.UnsLineId).ToListAsync(ct),
            NodeAcls = await db.NodeAcls.AsNoTracking().OrderBy(x => x.NodeAclId).ToListAsync(ct),
            Scripts = await db.Scripts.AsNoTracking().OrderBy(x => x.ScriptId).ToListAsync(ct),
            VirtualTags = await db.VirtualTags.AsNoTracking().OrderBy(x => x.VirtualTagId).ToListAsync(ct),
            ScriptedAlarms = await db.ScriptedAlarms.AsNoTracking().OrderBy(x => x.ScriptedAlarmId).ToListAsync(ct),
        };

        var blob = JsonSerializer.SerializeToUtf8Bytes(snapshot, JsonOptions);
        var hash = Convert.ToHexStringLower(SHA256.HashData(blob));
        return new ConfigArtifact(blob, hash);
    }

    /// <summary>Returns the SHA-256 hex digest of the supplied artifact bytes (lowercase, no prefix).</summary>
    /// <param name="blob">The bytes to hash.</param>
    /// <returns>The lowercase hex-encoded SHA-256 digest.</returns>
    public static string HashOf(ReadOnlySpan<byte> blob) =>
        Convert.ToHexStringLower(SHA256.HashData(blob));
}
