using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using ScadaLink.Commons.Types.Flattening;

namespace ScadaLink.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>
/// </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>
    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,
                    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,
                    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
                })
                .ToList()
        };

        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
    {
        public List<HashableAlarm> Alarms { get; init; } = [];
        public int? AreaId { get; init; }
        public List<HashableAttribute> Attributes { get; init; } = [];
        public string InstanceUniqueName { get; init; } = string.Empty;
        public List<HashableScript> Scripts { get; init; } = [];
        public int SiteId { get; init; }
        public int TemplateId { get; init; }
    }

    private sealed record HashableAttribute
    {
        public int? BoundDataConnectionId { get; init; }
        public string CanonicalName { get; init; } = string.Empty;
        public string? DataSourceReference { get; init; }
        public string DataType { get; init; } = string.Empty;
        public bool IsLocked { get; init; }
        public string? Value { get; init; }
    }

    private sealed record HashableAlarm
    {
        public string CanonicalName { get; init; } = string.Empty;
        public bool IsLocked { get; init; }
        public string? OnTriggerScriptCanonicalName { get; init; }
        public int PriorityLevel { get; init; }
        public string? TriggerConfiguration { get; init; }
        public string TriggerType { get; init; } = string.Empty;
    }

    private sealed record HashableScript
    {
        public string CanonicalName { get; init; } = string.Empty;
        public string Code { get; init; } = string.Empty;
        public bool IsLocked { get; init; }
        public long? MinTimeBetweenRunsTicks { get; init; }
        public string? ParameterDefinitions { get; init; }
        public string? ReturnDefinition { get; init; }
        public string? TriggerConfiguration { get; init; }
        public string? TriggerType { get; init; }
    }
}
