using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Messages.Management;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.TemplateEngine.Flattening;

namespace ZB.MOM.WW.ScadaBridge.TemplateEngine;

/// <summary>
/// Read-only AUTHORING resolver. Given a template id and the full
/// template lookup, computes the EFFECTIVE inherited member set fresh from the
/// whole inheritance chain (root -> leaf, arbitrary depth, cycle-guarded via
/// <see cref="TemplateResolver.BuildInheritanceChain"/>), annotated per member
/// with origin (own / inherited-from-X) and lock state, plus a staleness
/// summary comparing the template's STORED member rows against the resolved set.
///
/// <para>
/// The inheritance precedence here MIRRORS
/// <see cref="Flattening.FlatteningService"/>'s <c>ResolveInherited*</c> methods
/// EXACTLY so the editor preview agrees with what a deploy would produce:
/// walk base -> derived (derived wins); an <c>IsInherited</c> placeholder row on
/// a derived template never shadows the live base value (it only contributes a
/// row when no ancestor defines the member); a locked member, once seen, is not
/// overridden by a downstream template; an ancestor's <c>LockedInDerived</c>
/// flag is propagated so the editor can render the member read-only; and a
/// partial HiLo alarm override is MERGED per-setpoint against the inherited
/// trigger config (reusing <see cref="FlatteningService.MergeHiLoConfig"/>) so
/// the resolved trigger config equals deploy's.
/// </para>
///
/// <para>
/// This is inheritance only -- composition and instance overrides are the
/// instance flattener's concern (there are no instances in the
/// template-authoring context). It NEVER mutates stored rows and is not on the
/// deploy path.
/// </para>
/// </summary>
public static class TemplateInheritanceResolver
{
    /// <summary>
    /// Resolves the effective inherited member set for <paramref name="templateId"/>.
    /// </summary>
    /// <param name="templateId">The template to resolve members for.</param>
    /// <param name="allTemplates">The complete set of templates (for chain lookups).</param>
    /// <returns>
    /// The resolved member set (attributes / alarms / scripts / native alarm
    /// sources), each annotated with origin + lock state, plus the staleness
    /// summary. Returns an empty set when the template id is not found.
    /// </returns>
    public static ResolvedTemplateMembers Resolve(int templateId, IReadOnlyList<Template> allTemplates)
    {
        // Duplicate-tolerant lookup (matches TemplateResolver.ResolveAllMembers):
        // an Id of 0 is a valid node (import-staging / not-yet-saved rows).
        var lookup = CycleDetector.BuildLookup(allTemplates);
        if (!lookup.TryGetValue(templateId, out var self))
            return new ResolvedTemplateMembers { TemplateId = templateId };

        // Root -> leaf (derived last). The OWN template is the last element.
        var chain = TemplateResolver.BuildInheritanceChain(templateId, lookup);

        // Resolve winners ONCE per member type so projection and staleness share a
        // single source of truth (the same merged effective state).
        var attributeWinners = ResolveAttributeWinners(chain);
        var alarmWinners = ResolveAlarmWinners(chain);
        var scriptWinners = ResolveScriptWinners(chain);
        var nativeSourceWinners = ResolveNativeAlarmSourceWinners(chain);

        var attributes = Project(attributeWinners, templateId, w => w.EffectiveValue, _ => null);
        // The DTO EffectiveValue keeps surfacing the priority as the at-a-glance
        // preview; EffectiveTriggerConfiguration carries the merged trigger config.
        var alarms = Project(alarmWinners, templateId, w => w.Row.PriorityLevel.ToString(), w => w.EffectiveValue);
        var scripts = Project(scriptWinners, templateId, w => w.EffectiveValue, _ => null);
        var nativeAlarmSources = Project(nativeSourceWinners, templateId, w => w.EffectiveValue, _ => null);

        var staleness = ComputeStaleness(
            self, templateId, attributeWinners, alarmWinners, scriptWinners, nativeSourceWinners);

        return new ResolvedTemplateMembers
        {
            TemplateId = templateId,
            TemplateName = self.Name,
            ParentTemplateId = self.ParentTemplateId,
            Attributes = attributes,
            Alarms = alarms,
            Scripts = scripts,
            NativeAlarmSources = nativeAlarmSources,
            Staleness = staleness
        };
    }

    // -- per-member-type winners (origin-tracking) --

    /// <summary>
    /// The winning row for a member, plus where it came from. <c>EffectiveValue</c>
    /// is the resolved scalar/effective value for the member type -- usually the
    /// winning row's own field, but for HiLo alarms it is the PER-SETPOINT
    /// MERGED trigger configuration accumulated down the chain (see
    /// <c>mergeEffective</c> on <see cref="ResolveWinners{T}"/>), so the resolver
    /// preview equals what the flattener produces on deploy.
    /// </summary>
    private sealed record Winner<T>(T Row, Template Origin, bool BaseLocked, string? EffectiveValue);

    /// <summary>
    /// Generic base -> derived walk that picks the winning row per member name,
    /// replicating FlatteningService precedence:
    /// derived wins; an <c>IsInherited</c> placeholder does not shadow a live
    /// ancestor value; a locked member is not overridden downstream; the
    /// ancestor <c>LockedInDerived</c> flag is tracked.
    /// <para>
    /// <paramref name="valueOf"/> extracts the per-member effective value from a
    /// single row. <paramref name="mergeEffective"/> (optional) computes the
    /// effective value when a derived row OVERRIDES an existing winner -- for HiLo
    /// alarms this reuses <see cref="FlatteningService.MergeHiLoConfig"/>
    /// so a partial override (e.g. just <c>hi</c>) inherits the rest of the
    /// ancestor config, exactly as deploy does. The full existing <see cref="Winner{T}"/>
    /// is passed so the hook can inspect the inherited row's fields (e.g.
    /// <c>TriggerType</c>) before deciding whether to merge or whole-replace.
    /// When null, the winning row's own value is used verbatim (whole-replace).
    /// </para>
    /// </summary>
    private static Dictionary<string, Winner<T>> ResolveWinners<T>(
        IReadOnlyList<Template> chain,
        Func<Template, IEnumerable<T>> select,
        Func<T, string> nameOf,
        Func<T, bool> isInherited,
        Func<T, bool> isLocked,
        Func<T, bool> lockedInDerived,
        Func<T, string?> valueOf,
        Func<Winner<T>, T, string?>? mergeEffective = null)
    {
        var result = new Dictionary<string, Winner<T>>(StringComparer.Ordinal);

        // chain is root-first; walk root -> derived so the derived template
        // (last) wins, mirroring FlatteningService (which walks its
        // most-derived-first chain back-to-front for the same effect).
        foreach (var template in chain)
        {
            foreach (var row in select(template))
            {
                var name = nameOf(row);
                if (result.TryGetValue(name, out var existing))
                {
                    // A locked ancestor row is not overridden by a downstream template.
                    if (isLocked(existing.Row))
                    {
                        if (lockedInDerived(row))
                            result[name] = existing with { BaseLocked = true };
                        continue;
                    }
                    // IsInherited placeholders never shadow the live ancestor row.
                    if (isInherited(row))
                    {
                        if (lockedInDerived(row))
                            result[name] = existing with { BaseLocked = true };
                        continue;
                    }
                }

                var baseLocked = (existing?.BaseLocked ?? false) || lockedInDerived(row);
                // When a derived row overrides an existing winner, give the merge
                // hook a chance to fuse the two effective values (HiLo per-setpoint);
                // otherwise the overriding row's own value wins (whole-replace).
                var effective = existing != null && mergeEffective != null
                    ? mergeEffective(existing, row)
                    : valueOf(row);
                result[name] = new Winner<T>(row, template, baseLocked, effective);
            }
        }

        return result;
    }

    private static Dictionary<string, Winner<TemplateAttribute>> ResolveAttributeWinners(
        IReadOnlyList<Template> chain) =>
        ResolveWinners(
            chain,
            t => t.Attributes,
            a => a.Name,
            a => a.IsInherited,
            a => a.IsLocked,
            a => a.LockedInDerived,
            a => a.Value);

    private static Dictionary<string, Winner<TemplateAlarm>> ResolveAlarmWinners(
        IReadOnlyList<Template> chain) =>
        // Winner.EffectiveValue carries the PER-SETPOINT MERGED trigger config so
        // a partial HiLo override (e.g. just `hi`) inherits the rest from the
        // ancestor -- reusing FlatteningService.MergeHiLoConfig so the resolver
        // preview equals deploy. The merge is gated on BOTH the existing winner
        // AND the derived row being HiLo, matching FlatteningService.ResolveInheritedAlarms
        // (lines: alarm.TriggerType == HiLo && existing.TriggerType == nameof(HiLo)).
        // When only the derived side is HiLo (base is non-HiLo), derived config wins
        // verbatim — the same whole-replace path the flattener takes in that case.
        ResolveWinners(
            chain,
            t => t.Alarms,
            a => a.Name,
            a => a.IsInherited,
            a => a.IsLocked,
            a => a.LockedInDerived,
            a => a.TriggerConfiguration,
            (existingWinner, derived) =>
                existingWinner.Row.TriggerType == AlarmTriggerType.HiLo && derived.TriggerType == AlarmTriggerType.HiLo
                    ? FlatteningService.MergeHiLoConfig(existingWinner.EffectiveValue, derived.TriggerConfiguration)
                    : derived.TriggerConfiguration);

    private static Dictionary<string, Winner<TemplateScript>> ResolveScriptWinners(
        IReadOnlyList<Template> chain) =>
        ResolveWinners(
            chain,
            t => t.Scripts,
            s => s.Name,
            s => s.IsInherited,
            s => s.IsLocked,
            s => s.LockedInDerived,
            s => s.Code);

    private static Dictionary<string, Winner<TemplateNativeAlarmSource>> ResolveNativeAlarmSourceWinners(
        IReadOnlyList<Template> chain) =>
        ResolveWinners(
            chain,
            t => t.NativeAlarmSources,
            s => s.Name,
            s => s.IsInherited,
            s => s.IsLocked,
            s => s.LockedInDerived,
            s => s.SourceReference);

    private static IReadOnlyList<ResolvedTemplateMemberInfo> Project<T>(
        Dictionary<string, Winner<T>> winners,
        int selfId,
        Func<Winner<T>, string?> effectiveValue,
        Func<Winner<T>, string?> effectiveTriggerConfiguration)
    {
        return winners.Values
            .Select(w => new ResolvedTemplateMemberInfo
            {
                Name = NameOf(w.Row),
                IsInherited = w.Origin.Id != selfId,
                OriginTemplateId = w.Origin.Id,
                OriginTemplateName = w.Origin.Name,
                IsLocked = IsLockedOf(w.Row),
                IsBaseLocked = w.BaseLocked && w.Origin.Id != selfId,
                EffectiveValue = effectiveValue(w),
                EffectiveTriggerConfiguration = effectiveTriggerConfiguration(w)
            })
            .OrderBy(m => m.Name, StringComparer.Ordinal)
            .ToList();
    }

    private static string NameOf<T>(T row) => row switch
    {
        TemplateAttribute a => a.Name,
        TemplateAlarm a => a.Name,
        TemplateScript s => s.Name,
        TemplateNativeAlarmSource s => s.Name,
        _ => string.Empty
    };

    private static bool IsLockedOf<T>(T row) => row switch
    {
        TemplateAttribute a => a.IsLocked,
        TemplateAlarm a => a.IsLocked,
        TemplateScript s => s.IsLocked,
        TemplateNativeAlarmSource s => s.IsLocked,
        _ => false
    };

    // -- staleness --

    /// <summary>
    /// Compares the template's STORED member rows against the freshly-resolved
    /// set and counts how many INHERITED members drift: a freshly-resolved
    /// inherited member with no stored row, or whose stored placeholder differs
    /// from the freshly-resolved (effective) value. Own (non-inherited) members
    /// never count toward staleness -- they are authoritative by definition.
    /// <para>
    /// The comparison key per type is WIDE -- it covers the fields that actually
    /// matter, not a single scalar -- so a base change that leaves the previously
    /// compared scalar untouched (e.g. an alarm HiLo threshold change with the
    /// priority unchanged, or a native source ConnectionName/ConditionFilter
    /// change with the SourceReference unchanged) still registers as stale:
    /// </para>
    /// <list type="bullet">
    ///   <item><description>Attributes: <c>Value</c>.</description></item>
    ///   <item><description>Alarms: <c>PriorityLevel</c> + (merged) <c>TriggerConfiguration</c> + <c>Description</c> + <c>OnTriggerScriptId</c>.</description></item>
    ///   <item><description>Scripts: <c>Code</c>.</description></item>
    ///   <item><description>NativeAlarmSources: <c>ConnectionName</c> + <c>SourceReference</c> + <c>ConditionFilter</c>.</description></item>
    /// </list>
    /// </summary>
    private static ResolvedTemplateStaleness ComputeStaleness(
        Template self,
        int selfId,
        Dictionary<string, Winner<TemplateAttribute>> attributeWinners,
        Dictionary<string, Winner<TemplateAlarm>> alarmWinners,
        Dictionary<string, Winner<TemplateScript>> scriptWinners,
        Dictionary<string, Winner<TemplateNativeAlarmSource>> nativeSourceWinners)
    {
        int differing = 0;

        differing += CountDrift(
            attributeWinners, selfId,
            self.Attributes.ToDictionary(a => a.Name, a => AttributeKey(a.Value), StringComparer.Ordinal),
            w => AttributeKey(w.EffectiveValue));

        differing += CountDrift(
            alarmWinners, selfId,
            self.Alarms.ToDictionary(
                a => a.Name,
                a => AlarmKey(a.PriorityLevel, a.TriggerConfiguration, a.Description, a.OnTriggerScriptId),
                StringComparer.Ordinal),
            // The resolved (winner) side uses the MERGED effective trigger config.
            w => AlarmKey(w.Row.PriorityLevel, w.EffectiveValue, w.Row.Description, w.Row.OnTriggerScriptId));

        differing += CountDrift(
            scriptWinners, selfId,
            self.Scripts.ToDictionary(s => s.Name, s => ScriptKey(s.Code), StringComparer.Ordinal),
            w => ScriptKey(w.Row.Code));

        differing += CountDrift(
            nativeSourceWinners, selfId,
            self.NativeAlarmSources.ToDictionary(
                s => s.Name,
                s => NativeSourceKey(s.ConnectionName, s.SourceReference, s.ConditionFilter),
                StringComparer.Ordinal),
            w => NativeSourceKey(w.Row.ConnectionName, w.Row.SourceReference, w.Row.ConditionFilter));

        return new ResolvedTemplateStaleness
        {
            IsStale = differing > 0,
            DifferingMemberCount = differing
        };
    }

    /// <summary>
    /// Counts inherited winners that drift from their stored placeholder: a
    /// winner whose origin is an ANCESTOR (inherited) and whose composite key has
    /// no stored counterpart, or differs from the stored one.
    /// </summary>
    private static int CountDrift<T>(
        Dictionary<string, Winner<T>> winners,
        int selfId,
        IReadOnlyDictionary<string, string> storedKeysByName,
        Func<Winner<T>, string> resolvedKey)
    {
        int count = 0;
        foreach (var (name, w) in winners)
        {
            if (w.Origin.Id == selfId)
                continue; // own rows are authoritative -- never stale

            if (!storedKeysByName.TryGetValue(name, out var storedKey))
            {
                // Inherited member with no stored placeholder row (e.g. a base
                // member added after the derived template was created).
                count++;
                continue;
            }

            if (!string.Equals(storedKey, resolvedKey(w), StringComparison.Ordinal))
                count++; // stored placeholder is stale vs. the live (effective) base value
        }

        return count;
    }

    // -- composite comparison keys --
    // A unit-separator delimiter (U+001F, which never appears in JSON / names /
    // code) joins the fields; a distinct null marker (U+0000) ensures a null
    // field never collides with an empty one. The SAME normalization is applied
    // to both the stored side and the resolved side.
    private const string KeySep = "\u001F";
    private const string NullMarker = "\u0000";
    private static string Mark(string? s) => s ?? NullMarker;

    private static string AttributeKey(string? value) => Mark(value);

    private static string ScriptKey(string? code) => Mark(code);

    private static string AlarmKey(int priorityLevel, string? triggerConfiguration, string? description, int? onTriggerScriptId) =>
        string.Join(KeySep,
            priorityLevel.ToString(),
            Mark(triggerConfiguration),
            Mark(description),
            onTriggerScriptId?.ToString() ?? NullMarker);

    private static string NativeSourceKey(string? connectionName, string? sourceReference, string? conditionFilter) =>
        string.Join(KeySep, Mark(connectionName), Mark(sourceReference), Mark(conditionFilter));
}
