using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;

namespace ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening;

/// <summary>
/// Produces a deterministic SHA-256 hash of a FlattenedConfiguration.
/// Same content always produces the same hash across runs.
/// <para>
/// DETERMINISM CONTRACT: System.Text.Json serializes properties in CLR
/// declaration order, which it does NOT sort. Stable output therefore relies
/// on the private <c>Hashable*</c> records below declaring their properties
/// in alphabetical order. A contributor adding a property out of alphabetical
/// order would silently change every revision hash; the ordering is guarded
/// by <c>RevisionHashServiceTests.HashableRecords_PropertiesDeclaredAlphabetically</c>.
/// Collections are explicitly sorted by <c>CanonicalName</c> before hashing.
/// </para>
/// <para>
/// Native alarm source bindings
/// (<see cref="ResolvedNativeAlarmSource"/>) now participate in the hash so
/// that binding edits flag the instance stale. The hashable property is
/// populated NULL-WHEN-EMPTY and the serializer omits null properties
/// (<see cref="JsonIgnoreCondition.WhenWritingNull"/>), so every
/// native-source-free configuration keeps its byte-identical pre-migration
/// hash — no global staleness storm. Native-source-bearing instances will
/// report stale once after upgrade; redeploy clears it — deliberate.
/// </para>
/// </summary>
public class RevisionHashService
{
    private static readonly JsonSerializerOptions CanonicalJsonOptions = new()
    {
        PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
        DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull,
        WriteIndented = false
    };

    /// <summary>
    /// Computes a deterministic SHA-256 hash of the given flattened configuration.
    /// The hash is computed over a canonical JSON representation with sorted keys,
    /// excluding volatile fields like GeneratedAtUtc.
    /// </summary>
    /// <param name="configuration">The flattened configuration to hash.</param>
    /// <returns>A hex-encoded SHA-256 hash string of the canonical configuration representation.</returns>
    public string ComputeHash(FlattenedConfiguration configuration)
    {
        ArgumentNullException.ThrowIfNull(configuration);

        // Create a hashable representation that excludes volatile fields
        var hashInput = new HashableConfiguration
        {
            InstanceUniqueName = configuration.InstanceUniqueName,
            TemplateId = configuration.TemplateId,
            SiteId = configuration.SiteId,
            AreaId = configuration.AreaId,
            Attributes = configuration.Attributes
                .OrderBy(a => a.CanonicalName, StringComparer.Ordinal)
                .Select(a => new HashableAttribute
                {
                    CanonicalName = a.CanonicalName,
                    Value = a.Value,
                    DataType = a.DataType,
                    ElementDataType = a.ElementDataType,
                    Description = a.Description,
                    IsLocked = a.IsLocked,
                    DataSourceReference = a.DataSourceReference,
                    BoundDataConnectionId = a.BoundDataConnectionId
                })
                .ToList(),
            Alarms = configuration.Alarms
                .OrderBy(a => a.CanonicalName, StringComparer.Ordinal)
                .Select(a => new HashableAlarm
                {
                    CanonicalName = a.CanonicalName,
                    Description = a.Description,
                    PriorityLevel = a.PriorityLevel,
                    IsLocked = a.IsLocked,
                    TriggerType = a.TriggerType,
                    TriggerConfiguration = a.TriggerConfiguration,
                    OnTriggerScriptCanonicalName = a.OnTriggerScriptCanonicalName
                })
                .ToList(),
            Scripts = configuration.Scripts
                .OrderBy(s => s.CanonicalName, StringComparer.Ordinal)
                .Select(s => new HashableScript
                {
                    CanonicalName = s.CanonicalName,
                    Code = s.Code,
                    IsLocked = s.IsLocked,
                    TriggerType = s.TriggerType,
                    TriggerConfiguration = s.TriggerConfiguration,
                    ParameterDefinitions = s.ParameterDefinitions,
                    ReturnDefinition = s.ReturnDefinition,
                    MinTimeBetweenRunsTicks = s.MinTimeBetweenRuns?.Ticks,
                    // Include the per-script timeout so a change to it
                    // is detected as a configuration change (staleness/redeploy).
                    ExecutionTimeoutSeconds = s.ExecutionTimeoutSeconds
                })
                .ToList(),
            NativeAlarmSources = configuration.NativeAlarmSources is { Count: > 0 }
                ? configuration.NativeAlarmSources
                    .OrderBy(n => n.CanonicalName, StringComparer.Ordinal)
                    .Select(n => new HashableNativeAlarmSource
                    {
                        CanonicalName = n.CanonicalName,
                        ConditionFilter = n.ConditionFilter,
                        ConnectionName = n.ConnectionName,
                        IsLocked = n.IsLocked,
                        SourceReference = n.SourceReference
                    })
                    .ToList()
                : null,
            Connections = configuration.Connections is { Count: > 0 }
                ? new SortedDictionary<string, HashableConnection>(
                    configuration.Connections.ToDictionary(
                        kvp => kvp.Key,
                        kvp => new HashableConnection
                        {
                            BackupConfigurationJson = kvp.Value.BackupConfigurationJson,
                            ConfigurationJson = kvp.Value.ConfigurationJson,
                            FailoverRetryCount = kvp.Value.FailoverRetryCount,
                            Protocol = kvp.Value.Protocol
                        }),
                    StringComparer.Ordinal)
                : null
        };

        var json = JsonSerializer.Serialize(hashInput, CanonicalJsonOptions);
        var hashBytes = SHA256.HashData(Encoding.UTF8.GetBytes(json));
        return $"sha256:{Convert.ToHexStringLower(hashBytes)}";
    }

    // Internal types for deterministic serialization. Properties MUST be declared
    // in alphabetical order — System.Text.Json emits them in declaration order and
    // does not sort. See the DETERMINISM CONTRACT note on the class summary.
    private sealed record HashableConfiguration
    {
        /// <summary>
        /// Collection of alarms in the configuration.
        /// </summary>
        public List<HashableAlarm> Alarms { get; init; } = [];
        /// <summary>
        /// The area identifier.
        /// </summary>
        public int? AreaId { get; init; }
        /// <summary>
        /// Collection of attributes in the configuration.
        /// </summary>
        public List<HashableAttribute> Attributes { get; init; } = [];
        /// <summary>
        /// Data connection configurations keyed by connection name. Sorted by key
        /// (ordinal) to keep serialization deterministic. Null when the deployment
        /// package carries no connections.
        /// </summary>
        public SortedDictionary<string, HashableConnection>? Connections { get; init; }
        /// <summary>
        /// The unique instance name.
        /// </summary>
        public string InstanceUniqueName { get; init; } = string.Empty;
        /// <summary>
        /// Collection of native alarm source bindings in the configuration.
        /// Null when the configuration carries no native alarm sources so that
        /// native-source-free configs hash identically to before this field was
        /// added (see the MIGRATION note on the class summary).
        /// </summary>
        public List<HashableNativeAlarmSource>? NativeAlarmSources { get; init; }
        /// <summary>
        /// Collection of scripts in the configuration.
        /// </summary>
        public List<HashableScript> Scripts { get; init; } = [];
        /// <summary>
        /// The site identifier.
        /// </summary>
        public int SiteId { get; init; }
        /// <summary>
        /// The template identifier.
        /// </summary>
        public int TemplateId { get; init; }
    }

    private sealed record HashableAttribute
    {
        /// <summary>
        /// The data connection identifier the attribute is bound to.
        /// </summary>
        public int? BoundDataConnectionId { get; init; }
        /// <summary>
        /// The canonical name of the attribute.
        /// </summary>
        public string CanonicalName { get; init; } = string.Empty;
        /// <summary>
        /// The data source reference for the attribute.
        /// </summary>
        public string? DataSourceReference { get; init; }
        /// <summary>
        /// The data type of the attribute.
        /// </summary>
        public string DataType { get; init; } = string.Empty;
        /// <summary>
        /// The attribute description (authoring-time documentation that still
        /// travels with the deployed payload).
        /// </summary>
        public string? Description { get; init; }
        /// <summary>
        /// For List attributes: the element scalar type name; null otherwise.
        /// Folded into the hash so a List element-type change (e.g.
        /// Int32 → Double) with identical JSON-encoded values is detected as a
        /// staleness/revision change. Null for scalar attributes — and because
        /// the serializer ignores null properties
        /// (<see cref="JsonIgnoreCondition.WhenWritingNull"/>), scalar-only
        /// configurations hash identically to before this field was added.
        /// </summary>
        public string? ElementDataType { get; init; }
        /// <summary>
        /// Whether the attribute is locked.
        /// </summary>
        public bool IsLocked { get; init; }
        /// <summary>
        /// The attribute value.
        /// </summary>
        public string? Value { get; init; }
    }

    private sealed record HashableAlarm
    {
        /// <summary>
        /// The canonical name of the alarm.
        /// </summary>
        public string CanonicalName { get; init; } = string.Empty;
        /// <summary>
        /// The alarm description (authoring-time documentation that still
        /// travels with the deployed payload).
        /// </summary>
        public string? Description { get; init; }
        /// <summary>
        /// Whether the alarm is locked.
        /// </summary>
        public bool IsLocked { get; init; }
        /// <summary>
        /// The canonical name of the script triggered by this alarm.
        /// </summary>
        public string? OnTriggerScriptCanonicalName { get; init; }
        /// <summary>
        /// The priority level of the alarm.
        /// </summary>
        public int PriorityLevel { get; init; }
        /// <summary>
        /// The trigger configuration for the alarm.
        /// </summary>
        public string? TriggerConfiguration { get; init; }
        /// <summary>
        /// The trigger type of the alarm.
        /// </summary>
        public string TriggerType { get; init; } = string.Empty;
    }

    private sealed record HashableConnection
    {
        /// <summary>
        /// Backup connection configuration JSON, if any.
        /// </summary>
        public string? BackupConfigurationJson { get; init; }
        /// <summary>
        /// Primary connection configuration JSON.
        /// </summary>
        public string? ConfigurationJson { get; init; }
        /// <summary>
        /// Number of failover retries before giving up.
        /// </summary>
        public int FailoverRetryCount { get; init; }
        /// <summary>
        /// Protocol name (e.g. "OpcUa").
        /// </summary>
        public string Protocol { get; init; } = string.Empty;
    }

    private sealed record HashableNativeAlarmSource
    {
        /// <summary>
        /// The path-qualified canonical name of the native alarm source binding.
        /// </summary>
        public string CanonicalName { get; init; } = string.Empty;
        /// <summary>
        /// Optional condition filter; null = mirror all conditions under the source.
        /// </summary>
        public string? ConditionFilter { get; init; }
        /// <summary>
        /// The data connection name that owns the alarm feed.
        /// </summary>
        public string ConnectionName { get; init; } = string.Empty;
        /// <summary>
        /// Whether the native alarm source binding is locked.
        /// </summary>
        public bool IsLocked { get; init; }
        /// <summary>
        /// The source reference (OPC UA SourceNode/notifier nodeId, or MxAccess object/area).
        /// </summary>
        public string SourceReference { get; init; } = string.Empty;
    }

    private sealed record HashableScript
    {
        /// <summary>
        /// The canonical name of the script.
        /// </summary>
        public string CanonicalName { get; init; } = string.Empty;
        /// <summary>
        /// The script code.
        /// </summary>
        public string Code { get; init; } = string.Empty;
        /// <summary>
        /// The per-script execution timeout in seconds (null = global).
        /// </summary>
        public int? ExecutionTimeoutSeconds { get; init; }
        /// <summary>
        /// Whether the script is locked.
        /// </summary>
        public bool IsLocked { get; init; }
        /// <summary>
        /// The minimum time between runs in ticks.
        /// </summary>
        public long? MinTimeBetweenRunsTicks { get; init; }
        /// <summary>
        /// JSON representation of parameter definitions.
        /// </summary>
        public string? ParameterDefinitions { get; init; }
        /// <summary>
        /// JSON representation of the return type definition.
        /// </summary>
        public string? ReturnDefinition { get; init; }
        /// <summary>
        /// The trigger configuration for the script.
        /// </summary>
        public string? TriggerConfiguration { get; init; }
        /// <summary>
        /// The trigger type of the script.
        /// </summary>
        public string? TriggerType { get; init; }
    }
}
