using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Transport.Serialization;

namespace ZB.MOM.WW.ScadaBridge.Transport.Import;

/// <summary>
/// The single source of truth for "has this template child changed?": the
/// field-for-field equality between a persisted template-child entity and its bundle
/// DTO. Every writable field is compared.
/// <para>
/// BOTH the Preview diff (<see cref="ArtifactDiff.CompareTemplate"/>) and the
/// Overwrite child-sync (<c>BundleImporter.SyncTemplate*Async</c>) MUST route their
/// change detection through these methods so a Preview can never disagree with what
/// an Overwrite actually writes. Historically the two drifted — the diff omitted
/// <c>ElementDataType</c>, <c>ExecutionTimeoutSeconds</c>, the alarm on-trigger
/// script, and <c>LockedInDerived</c> — reporting <c>Identical</c> for artifacts an
/// Overwrite would in fact mutate. Adding a writable field to any template-child
/// entity means adding it here, once.
/// </para>
/// </summary>
internal static class TemplateChildEquality
{
    /// <summary>
    /// True when the persisted attribute equals its bundle DTO across every writable
    /// field. The single source of truth for "attribute changed"; both ArtifactDiff
    /// and <c>SyncTemplateAttributesAsync</c> call it.
    /// <para>
    /// Callers that normalise the DTO <c>Value</c> before comparing (the importer
    /// normalises List values) pass a DTO whose <c>Value</c> is already normalised.
    /// </para>
    /// </summary>
    /// <param name="e">The persisted template attribute.</param>
    /// <param name="i">The bundle DTO to compare against.</param>
    /// <returns><c>true</c> when every writable field matches.</returns>
    public static bool AttributesEqual(TemplateAttribute e, TemplateAttributeDto i) =>
        e.Value == i.Value
        && e.DataType == i.DataType
        && e.ElementDataType == i.ElementDataType
        && e.IsLocked == i.IsLocked
        && e.LockedInDerived == i.LockedInDerived
        && e.Description == i.Description
        && e.DataSourceReference == i.DataSourceReference;

    /// <summary>
    /// True when the persisted alarm equals its bundle DTO across every writable
    /// field, including the on-trigger script binding. Bundles reference the
    /// on-trigger script by NAME (ids are environment-specific), so the caller
    /// supplies <paramref name="scriptNameById"/> — a resolver over the alarm's own
    /// template scripts — to map the persisted <c>OnTriggerScriptId</c> FK back to a
    /// name for comparison. The single source of truth for "alarm changed"; both
    /// ArtifactDiff and <c>SyncTemplateAlarmsAsync</c> call it.
    /// </summary>
    /// <param name="e">The persisted template alarm.</param>
    /// <param name="i">The bundle DTO to compare against.</param>
    /// <param name="scriptNameById">Resolver mapping an on-trigger script id to its name.</param>
    /// <returns><c>true</c> when every writable field matches.</returns>
    public static bool AlarmsEqual(TemplateAlarm e, TemplateAlarmDto i, Func<int?, string?> scriptNameById) =>
        e.Description == i.Description
        && e.PriorityLevel == i.PriorityLevel
        && e.TriggerType == i.TriggerType
        && e.TriggerConfiguration == i.TriggerConfiguration
        && e.IsLocked == i.IsLocked
        && e.LockedInDerived == i.LockedInDerived
        && scriptNameById(e.OnTriggerScriptId) == i.OnTriggerScriptName;

    /// <summary>
    /// True when the persisted script equals its bundle DTO across every writable
    /// field. The single source of truth for "script changed"; both ArtifactDiff
    /// (via <c>DiffScriptChildren</c>) and <c>SyncTemplateScriptsAsync</c> call it.
    /// </summary>
    /// <param name="e">The persisted template script.</param>
    /// <param name="i">The bundle DTO to compare against.</param>
    /// <returns><c>true</c> when every writable field matches.</returns>
    public static bool ScriptsEqual(TemplateScript e, TemplateScriptDto i) =>
        string.Equals(e.Code, i.Code, StringComparison.Ordinal)
        && e.TriggerType == i.TriggerType
        && e.TriggerConfiguration == i.TriggerConfiguration
        && e.ParameterDefinitions == i.ParameterDefinitions
        && e.ReturnDefinition == i.ReturnDefinition
        && e.IsLocked == i.IsLocked
        && e.LockedInDerived == i.LockedInDerived
        && e.MinTimeBetweenRuns == i.MinTimeBetweenRuns
        && e.ExecutionTimeoutSeconds == i.ExecutionTimeoutSeconds;

    /// <summary>
    /// True when the persisted native alarm source equals its bundle DTO across every
    /// writable field. The single source of truth for "native alarm source changed";
    /// both ArtifactDiff and <c>SyncTemplateNativeAlarmSourcesAsync</c> call it.
    /// </summary>
    /// <param name="e">The persisted native alarm source.</param>
    /// <param name="i">The bundle DTO to compare against.</param>
    /// <returns><c>true</c> when every writable field matches.</returns>
    public static bool NativeAlarmSourcesEqual(TemplateNativeAlarmSource e, TemplateNativeAlarmSourceDto i) =>
        e.Description == i.Description
        && e.ConnectionName == i.ConnectionName
        && e.SourceReference == i.SourceReference
        && e.ConditionFilter == i.ConditionFilter
        && e.IsLocked == i.IsLocked
        && e.IsInherited == i.IsInherited
        && e.LockedInDerived == i.LockedInDerived;

    /// <summary>
    /// Builds a resolver mapping a template script id → its name over the given
    /// scripts, for the alarm on-trigger comparison. Returns null for a null id or an
    /// id absent from the set.
    /// </summary>
    /// <param name="scripts">The template scripts to index by id.</param>
    /// <returns>A resolver mapping a script id to its name, or null when unresolved.</returns>
    public static Func<int?, string?> ScriptNameResolver(IEnumerable<TemplateScript> scripts)
    {
        var byId = new Dictionary<int, string>();
        foreach (var s in scripts)
        {
            byId[s.Id] = s.Name;
        }

        return id => id is { } sid && byId.TryGetValue(sid, out var name) ? name : null;
    }
}
