using System.Text.Json;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Instances;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Sites;
using ZB.MOM.WW.ScadaBridge.Commons.Entities.Templates;
using ZB.MOM.WW.ScadaBridge.Commons.Types;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Enums;
using ZB.MOM.WW.ScadaBridge.Commons.Types.Flattening;

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

/// <summary>
/// Implements the template flattening algorithm.
/// Takes a template inheritance/composition graph plus instance overrides and connection bindings,
/// and produces a fully resolved FlattenedConfiguration.
///
/// Resolution order (most specific wins):
/// 1. Instance overrides (highest priority)
/// 2. Child template (most derived first in inheritance chain)
/// 3. Parent templates (walking up inheritance chain)
/// 4. Composed modules (recursively flattened with path-qualified canonical names)
///
/// Locked fields cannot be overridden by instance overrides.
/// </summary>
public class FlatteningService
{
    /// <summary>
    /// Produces a fully flattened configuration for an instance.
    /// </summary>
    /// <param name="instance">The instance to flatten.</param>
    /// <param name="templateChain">
    /// The inheritance chain from most-derived to root (index 0 = the instance's template,
    /// last = the ultimate base template). Each template includes its own attributes, alarms, scripts.
    /// </param>
    /// <param name="compositionMap">
    /// Map of template ID → list of compositions (composed module definitions).
    /// For each composition, the key is the parent template ID and the value includes the
    /// composed template's resolved chain.
    /// </param>
    /// <param name="composedTemplateChains">
    /// Map of composed template ID → its inheritance chain (same format as templateChain).
    /// </param>
    /// <param name="dataConnections">
    /// Available data connections for resolving connection bindings.
    /// </param>
    /// <returns>A Result containing the FlattenedConfiguration or an error message.</returns>
    public Result<FlattenedConfiguration> Flatten(
        Instance instance,
        IReadOnlyList<Template> templateChain,
        IReadOnlyDictionary<int, IReadOnlyList<TemplateComposition>> compositionMap,
        IReadOnlyDictionary<int, IReadOnlyList<Template>> composedTemplateChains,
        IReadOnlyDictionary<int, DataConnection> dataConnections)
    {
        if (templateChain.Count == 0)
            return Result<FlattenedConfiguration>.Failure("Template chain is empty.");

        try
        {
            // Step 0: Validate LockedInDerived isn't violated by any chain.
            var lockError = ValidateLockedInDerived(templateChain);
            if (lockError != null)
                return Result<FlattenedConfiguration>.Failure(lockError);
            foreach (var composedChain in composedTemplateChains.Values)
            {
                lockError = ValidateLockedInDerived(composedChain);
                if (lockError != null)
                    return Result<FlattenedConfiguration>.Failure(lockError);
            }

            // Step 1: Resolve attributes from inheritance chain (most-derived-first wins for same name)
            var attributes = ResolveInheritedAttributes(templateChain);

            // Step 2: Resolve composed module attributes with path-qualified names
            ResolveComposedAttributes(templateChain, compositionMap, composedTemplateChains, attributes);

            // Step 3: Apply instance overrides (respecting locks)
            ApplyInstanceOverrides(instance.AttributeOverrides, attributes);

            // Step 4: Apply connection bindings
            ApplyConnectionBindings(instance.ConnectionBindings, attributes, dataConnections);

            // Step 5: Resolve alarms from inheritance chain.
            // alarmScriptIds maps a resolved alarm's canonical name to the
            // TemplateScript.Id of its on-trigger script (if any).
            var alarmScriptIds = new Dictionary<string, int>(StringComparer.Ordinal);
            var alarms = ResolveInheritedAlarms(templateChain, prefix: null, alarmScriptIds);
            ResolveComposedAlarms(templateChain, compositionMap, composedTemplateChains, alarms, alarmScriptIds);
            ApplyInstanceAlarmOverrides(instance.AlarmOverrides, alarms);

            // Step 6: Resolve scripts from inheritance chain.
            // scriptCanonicalById maps a TemplateScript.Id to its resolved
            // canonical name, used to wire up alarm on-trigger script refs.
            var scriptCanonicalById = new Dictionary<int, string>();
            var scripts = ResolveInheritedScripts(templateChain, prefix: null, scriptCanonicalById);
            ResolveComposedScripts(templateChain, compositionMap, composedTemplateChains, scripts, scriptCanonicalById);

            // Step 7: Resolve alarm on-trigger script references to canonical names
            ResolveAlarmScriptReferences(alarms, alarmScriptIds, scriptCanonicalById);

            // Step 7b: Resolve native alarm source bindings (connection + source
            // reference). Conditions under each source are discovered at runtime;
            // flattening only resolves the binding through inherit/compose/override.
            var nativeAlarmSources = ResolveInheritedNativeAlarmSources(templateChain, prefix: null);
            ResolveComposedNativeAlarmSources(templateChain, compositionMap, composedTemplateChains, nativeAlarmSources);
            ApplyInstanceNativeAlarmSourceOverrides(instance.NativeAlarmSourceOverrides, nativeAlarmSources);

            // Step 8: Collect connection configurations for deployment packaging
            var connections = new Dictionary<string, ConnectionConfig>();
            foreach (var attr in attributes.Values)
            {
                if (attr.BoundDataConnectionId.HasValue &&
                    !string.IsNullOrEmpty(attr.BoundDataConnectionName) &&
                    !connections.ContainsKey(attr.BoundDataConnectionName))
                {
                    if (dataConnections.TryGetValue(attr.BoundDataConnectionId.Value, out var conn))
                    {
                        connections[attr.BoundDataConnectionName] = new ConnectionConfig
                        {
                            Protocol = conn.Protocol,
                            ConfigurationJson = conn.PrimaryConfiguration,
                            BackupConfigurationJson = conn.BackupConfiguration,
                            FailoverRetryCount = conn.FailoverRetryCount
                        };
                    }
                }
            }

            var config = new FlattenedConfiguration
            {
                InstanceUniqueName = instance.UniqueName,
                TemplateId = instance.TemplateId,
                SiteId = instance.SiteId,
                AreaId = instance.AreaId,
                Attributes = attributes.Values.OrderBy(a => a.CanonicalName, StringComparer.Ordinal).ToList(),
                Alarms = alarms.Values.OrderBy(a => a.CanonicalName, StringComparer.Ordinal).ToList(),
                NativeAlarmSources = nativeAlarmSources.Values.OrderBy(s => s.CanonicalName, StringComparer.Ordinal).ToList(),
                Scripts = scripts.Values.OrderBy(s => s.CanonicalName, StringComparer.Ordinal).ToList(),
                Connections = connections.Count > 0 ? connections : null,
                GeneratedAtUtc = DateTimeOffset.UtcNow
            };

            return Result<FlattenedConfiguration>.Success(config);
        }
        catch (Exception ex)
        {
            return Result<FlattenedConfiguration>.Failure($"Flattening failed: {ex.Message}");
        }
    }

    private static Dictionary<string, ResolvedAttribute> ResolveInheritedAttributes(
        IReadOnlyList<Template> templateChain)
    {
        var result = new Dictionary<string, ResolvedAttribute>(StringComparer.Ordinal);

        // Walk from base (last) to most-derived (first) so derived values win.
        // IsInherited rows on a derived template are placeholders that should
        // not shadow the live base value; they only contribute a row when the
        // base lacks one.
        for (int i = templateChain.Count - 1; i >= 0; i--)
        {
            var template = templateChain[i];
            var source = i == 0 ? "Template" : "Inherited";

            foreach (var attr in template.Attributes)
            {
                if (result.TryGetValue(attr.Name, out var existing))
                {
                    if (existing.IsLocked)
                        continue;
                    if (attr.IsInherited)
                        continue;
                }

                result[attr.Name] = new ResolvedAttribute
                {
                    CanonicalName = attr.Name,
                    Value = attr.Value,
                    DataType = attr.DataType.ToString(),
                    ElementDataType = attr.ElementDataType?.ToString(),
                    IsLocked = attr.IsLocked,
                    Description = attr.Description,
                    DataSourceReference = attr.DataSourceReference,
                    Source = source
                };
            }
        }

        return result;
    }

    /// <summary>
    /// Reports any LockedInDerived violations across the chain — i.e., a base
    /// attribute/alarm/script marked LockedInDerived that a downstream derived
    /// template overrides (IsInherited=false). Returns null on success or an
    /// error message describing the first offending entries.
    /// </summary>
    private static string? ValidateLockedInDerived(IReadOnlyList<Template> templateChain)
    {
        var attrLocks = new Dictionary<string, Template>(StringComparer.Ordinal);
        var alarmLocks = new Dictionary<string, Template>(StringComparer.Ordinal);
        var scriptLocks = new Dictionary<string, Template>(StringComparer.Ordinal);
        var errors = new List<string>();

        for (int i = templateChain.Count - 1; i >= 0; i--)
        {
            var template = templateChain[i];

            foreach (var attr in template.Attributes)
            {
                if (attr.LockedInDerived)
                    attrLocks[attr.Name] = template;
                else if (!attr.IsInherited && attrLocks.TryGetValue(attr.Name, out var lockingTemplate) && lockingTemplate.Id != template.Id)
                    errors.Add($"Attribute '{attr.Name}' is LockedInDerived by base template '{lockingTemplate.Name}' and cannot be overridden by '{template.Name}'.");
            }

            foreach (var alarm in template.Alarms)
            {
                if (alarm.LockedInDerived)
                    alarmLocks[alarm.Name] = template;
                else if (!alarm.IsInherited && alarmLocks.TryGetValue(alarm.Name, out var lockingTemplate) && lockingTemplate.Id != template.Id)
                    errors.Add($"Alarm '{alarm.Name}' is LockedInDerived by base template '{lockingTemplate.Name}' and cannot be overridden by '{template.Name}'.");
            }

            foreach (var script in template.Scripts)
            {
                if (script.LockedInDerived)
                    scriptLocks[script.Name] = template;
                else if (!script.IsInherited && scriptLocks.TryGetValue(script.Name, out var lockingTemplate) && lockingTemplate.Id != template.Id)
                    errors.Add($"Script '{script.Name}' is LockedInDerived by base template '{lockingTemplate.Name}' and cannot be overridden by '{template.Name}'.");
            }
        }

        return errors.Count == 0 ? null : string.Join(" ", errors);
    }

    private static void ResolveComposedAttributes(
        IReadOnlyList<Template> templateChain,
        IReadOnlyDictionary<int, IReadOnlyList<TemplateComposition>> compositionMap,
        IReadOnlyDictionary<int, IReadOnlyList<Template>> composedTemplateChains,
        Dictionary<string, ResolvedAttribute> attributes)
    {
        // Process compositions from each template in the chain
        foreach (var template in templateChain)
        {
            if (!compositionMap.TryGetValue(template.Id, out var compositions))
                continue;

            foreach (var composition in compositions)
                ResolveComposedAttributesRecursive(
                    composition, composition.InstanceName,
                    compositionMap, composedTemplateChains, attributes, new HashSet<int>());
        }
    }

    /// <summary>
    /// Recursively resolves the attributes of a composed module and every
    /// module nested inside it (to arbitrary depth), path-qualifying each
    /// canonical name with the accumulated <paramref name="prefix"/>.
    /// </summary>
    private static void ResolveComposedAttributesRecursive(
        TemplateComposition composition,
        string prefix,
        IReadOnlyDictionary<int, IReadOnlyList<TemplateComposition>> compositionMap,
        IReadOnlyDictionary<int, IReadOnlyList<Template>> composedTemplateChains,
        Dictionary<string, ResolvedAttribute> attributes,
        HashSet<int> path)
    {
        if (!composedTemplateChains.TryGetValue(composition.ComposedTemplateId, out var composedChain))
            return;

        var composedAttrs = ResolveInheritedAttributes(composedChain);
        foreach (var (name, attr) in composedAttrs)
        {
            var canonicalName = $"{prefix}.{name}";
            // Don't overwrite if already defined (most-derived wins)
            if (!attributes.ContainsKey(canonicalName))
            {
                attributes[canonicalName] = attr with
                {
                    CanonicalName = canonicalName,
                    Source = "Composed"
                };
            }
        }

        // Descend into nested compositions of every template in the chain.
        // Guard on the recursion PATH, not globally: composition is by-slot, so a
        // template legitimately appears twice under different prefixes; only a
        // template already on the CURRENT path is a cycle.
        foreach (var composedTemplate in composedChain)
        {
            if (!path.Add(composedTemplate.Id))
                continue;
            try
            {
                if (!compositionMap.TryGetValue(composedTemplate.Id, out var nestedCompositions))
                    continue;

                foreach (var nested in nestedCompositions)
                    ResolveComposedAttributesRecursive(
                        nested, $"{prefix}.{nested.InstanceName}",
                        compositionMap, composedTemplateChains, attributes, path);
            }
            finally
            {
                path.Remove(composedTemplate.Id);
            }
        }
    }

    private static void ApplyInstanceOverrides(
        ICollection<InstanceAttributeOverride> overrides,
        Dictionary<string, ResolvedAttribute> attributes)
    {
        foreach (var ovr in overrides)
        {
            if (!attributes.TryGetValue(ovr.AttributeName, out var existing))
                continue; // Cannot add new attributes via overrides

            if (existing.IsLocked)
                continue; // Locked attributes cannot be overridden

            attributes[ovr.AttributeName] = existing with
            {
                Value = ovr.OverrideValue,
                Source = "Override"
            };
        }
    }

    /// <summary>
    /// Applies per-instance alarm overrides on top of the
    /// inheritance-and-composition resolved alarms. Skips overrides for
    /// alarms that are locked at the template level. For HiLo triggers the
    /// override JSON is merged setpoint-by-setpoint (preserving inherited
    /// keys not present in the override); for other trigger types the
    /// override replaces the whole TriggerConfiguration.
    /// </summary>
    private static void ApplyInstanceAlarmOverrides(
        ICollection<InstanceAlarmOverride> overrides,
        Dictionary<string, ResolvedAlarm> alarms)
    {
        foreach (var ovr in overrides)
        {
            if (!alarms.TryGetValue(ovr.AlarmCanonicalName, out var existing))
                continue; // Cannot add new alarms via overrides

            if (existing.IsLocked)
                continue; // Locked alarms cannot be overridden

            var newConfig = existing.TriggerConfiguration;
            if (!string.IsNullOrWhiteSpace(ovr.TriggerConfigurationOverride))
            {
                newConfig = existing.TriggerType == nameof(AlarmTriggerType.HiLo)
                    ? MergeHiLoConfig(existing.TriggerConfiguration, ovr.TriggerConfigurationOverride)
                    : ovr.TriggerConfigurationOverride;
            }

            alarms[ovr.AlarmCanonicalName] = existing with
            {
                TriggerConfiguration = newConfig,
                PriorityLevel = ovr.PriorityLevelOverride ?? existing.PriorityLevel,
                Source = "Override"
            };
        }
    }

    private static void ApplyConnectionBindings(
        ICollection<InstanceConnectionBinding> bindings,
        Dictionary<string, ResolvedAttribute> attributes,
        IReadOnlyDictionary<int, DataConnection> dataConnections)
    {
        foreach (var binding in bindings)
        {
            if (!attributes.TryGetValue(binding.AttributeName, out var existing))
                continue;

            if (existing.DataSourceReference == null)
                continue; // Only data-sourced attributes can have connection bindings

            if (!dataConnections.TryGetValue(binding.DataConnectionId, out var connection))
                continue;

            attributes[binding.AttributeName] = existing with
            {
                BoundDataConnectionId = connection.Id,
                BoundDataConnectionName = connection.Name,
                BoundDataConnectionProtocol = connection.Protocol,
                DataSourceReference = binding.DataSourceReferenceOverride ?? existing.DataSourceReference
            };
        }
    }

    /// <summary>
    /// Resolves alarms from an inheritance chain. When <paramref name="prefix"/>
    /// is non-null the alarm names are returned bare (caller path-qualifies);
    /// the keys of the returned dictionary are always bare alarm names.
    /// <paramref name="alarmScriptIds"/> is populated with the on-trigger
    /// script id of each resolved alarm keyed by the canonical name the alarm
    /// will ultimately carry (bare name when <paramref name="prefix"/> is null,
    /// otherwise <c>prefix.name</c>).
    /// </summary>
    private static Dictionary<string, ResolvedAlarm> ResolveInheritedAlarms(
        IReadOnlyList<Template> templateChain,
        string? prefix,
        Dictionary<string, int> alarmScriptIds)
    {
        var result = new Dictionary<string, ResolvedAlarm>(StringComparer.Ordinal);
        var scriptIdByName = new Dictionary<string, int>(StringComparer.Ordinal);

        for (int i = templateChain.Count - 1; i >= 0; i--)
        {
            var template = templateChain[i];
            var source = i == 0 ? "Template" : "Inherited";

            foreach (var alarm in template.Alarms)
            {
                if (result.TryGetValue(alarm.Name, out var existing))
                {
                    if (existing.IsLocked)
                        continue;
                    // IsInherited rows on a derived template are placeholders
                    // that must not shadow the live base alarm; they only
                    // contribute a row when the base lacks one.
                    if (alarm.IsInherited)
                        continue;
                }

                // HiLo per-setpoint override: derived templates can supply a
                // partial TriggerConfiguration (e.g., just `hi`) and have the
                // remaining setpoints inherited. Other trigger types replace
                // the whole config on override (current behavior).
                var triggerConfig = alarm.TriggerConfiguration;
                if (existing != null
                    && alarm.TriggerType == AlarmTriggerType.HiLo
                    && existing.TriggerType == nameof(AlarmTriggerType.HiLo))
                {
                    triggerConfig = MergeHiLoConfig(existing.TriggerConfiguration, triggerConfig);
                }

                result[alarm.Name] = new ResolvedAlarm
                {
                    CanonicalName = alarm.Name,
                    Description = alarm.Description,
                    PriorityLevel = alarm.PriorityLevel,
                    IsLocked = alarm.IsLocked,
                    TriggerType = alarm.TriggerType.ToString(),
                    TriggerConfiguration = triggerConfig,
                    OnTriggerScriptCanonicalName = null, // Resolved later
                    Source = source
                };

                if (alarm.OnTriggerScriptId.HasValue)
                    scriptIdByName[alarm.Name] = alarm.OnTriggerScriptId.Value;
                else
                    scriptIdByName.Remove(alarm.Name);
            }
        }

        foreach (var (name, scriptId) in scriptIdByName)
        {
            var canonical = prefix == null ? name : $"{prefix}.{name}";
            alarmScriptIds[canonical] = scriptId;
        }

        return result;
    }

    /// <summary>
    /// Merges a derived HiLo trigger configuration onto an inherited one.
    /// Top-level keys present in <paramref name="derivedJson"/> override the
    /// inherited values; keys absent in the derived config are inherited.
    /// Returns the derived config verbatim on parse failure of either input —
    /// the existing whole-replace behavior is the safe fallback.
    /// </summary>
    /// <param name="inheritedJson">The parent template's HiLo trigger JSON, or null.</param>
    /// <param name="derivedJson">The child template's HiLo trigger JSON override, or null.</param>
    /// <returns>The merged HiLo config JSON, or the derived config on parse failure of either input.</returns>
    public static string? MergeHiLoConfig(string? inheritedJson, string? derivedJson)
    {
        if (string.IsNullOrWhiteSpace(inheritedJson)) return derivedJson;
        if (string.IsNullOrWhiteSpace(derivedJson)) return inheritedJson;

        try
        {
            using var inheritedDoc = JsonDocument.Parse(inheritedJson);
            using var derivedDoc = JsonDocument.Parse(derivedJson);

            if (inheritedDoc.RootElement.ValueKind != JsonValueKind.Object
                || derivedDoc.RootElement.ValueKind != JsonValueKind.Object)
            {
                return derivedJson;
            }

            using var stream = new MemoryStream();
            using (var writer = new Utf8JsonWriter(stream))
            {
                writer.WriteStartObject();

                var derivedKeys = new HashSet<string>(StringComparer.Ordinal);
                foreach (var prop in derivedDoc.RootElement.EnumerateObject())
                    derivedKeys.Add(prop.Name);

                // Inherited keys not present in derived survive.
                foreach (var prop in inheritedDoc.RootElement.EnumerateObject())
                {
                    if (derivedKeys.Contains(prop.Name)) continue;
                    prop.WriteTo(writer);
                }

                // Derived keys win.
                foreach (var prop in derivedDoc.RootElement.EnumerateObject())
                {
                    prop.WriteTo(writer);
                }

                writer.WriteEndObject();
            }
            return System.Text.Encoding.UTF8.GetString(stream.ToArray());
        }
        catch (JsonException)
        {
            return derivedJson;
        }
    }

    /// <summary>
    /// Computes the minimal HiLo override JSON given the inherited config and
    /// an edited config — returns only the top-level keys whose values differ
    /// from the inherited config. Returns <c>null</c> when no keys differ (the
    /// caller should treat that as "no override").
    ///
    /// Value comparison is type-aware so that JSON-escape differences (e.g.,
    /// a literal em-dash in the inherited config vs. <c>—</c> in the
    /// editor's serialized output) don't produce false-positive diffs. On
    /// parse failure of either input, returns <paramref name="editedJson"/>
    /// verbatim — safe fallback that matches the existing whole-replace
    /// semantics.
    /// </summary>
    /// <param name="inheritedJson">The parent template's HiLo trigger JSON, or null.</param>
    /// <param name="editedJson">The user-edited HiLo trigger JSON, or null.</param>
    /// <returns>A JSON string containing only the keys that differ from the inherited config, or null if there are no differences.</returns>
    public static string? DiffHiLoConfig(string? inheritedJson, string? editedJson)
    {
        if (string.IsNullOrWhiteSpace(editedJson)) return null;
        if (string.IsNullOrWhiteSpace(inheritedJson)) return editedJson;

        try
        {
            using var inheritedDoc = JsonDocument.Parse(inheritedJson);
            using var editedDoc = JsonDocument.Parse(editedJson);

            if (inheritedDoc.RootElement.ValueKind != JsonValueKind.Object
                || editedDoc.RootElement.ValueKind != JsonValueKind.Object)
            {
                return editedJson;
            }

            var changed = new List<JsonProperty>();
            foreach (var prop in editedDoc.RootElement.EnumerateObject())
            {
                if (!inheritedDoc.RootElement.TryGetProperty(prop.Name, out var inhProp))
                {
                    changed.Add(prop);
                    continue;
                }
                if (!ValuesEquivalent(prop.Value, inhProp))
                    changed.Add(prop);
            }

            if (changed.Count == 0) return null;

            using var stream = new MemoryStream();
            using (var writer = new Utf8JsonWriter(stream))
            {
                writer.WriteStartObject();
                foreach (var p in changed) p.WriteTo(writer);
                writer.WriteEndObject();
            }
            return System.Text.Encoding.UTF8.GetString(stream.ToArray());
        }
        catch (JsonException)
        {
            return editedJson;
        }
    }

    /// <summary>
    /// Compares two JSON values by their decoded meaning rather than their
    /// raw text. Strings are unescaped before comparison so equivalent values
    /// in different escape forms (e.g., a literal "—" vs. "—") match.
    /// Numbers compare by their double value so trailing-zero differences
    /// don't produce false diffs.
    /// </summary>
    private static bool ValuesEquivalent(JsonElement a, JsonElement b)
    {
        if (a.ValueKind != b.ValueKind) return false;
        return a.ValueKind switch
        {
            JsonValueKind.String => a.GetString() == b.GetString(),
            JsonValueKind.Number => a.GetDouble() == b.GetDouble(),
            JsonValueKind.True or JsonValueKind.False or JsonValueKind.Null => true,
            _ => a.GetRawText() == b.GetRawText()
        };
    }

    private static void ResolveComposedAlarms(
        IReadOnlyList<Template> templateChain,
        IReadOnlyDictionary<int, IReadOnlyList<TemplateComposition>> compositionMap,
        IReadOnlyDictionary<int, IReadOnlyList<Template>> composedTemplateChains,
        Dictionary<string, ResolvedAlarm> alarms,
        Dictionary<string, int> alarmScriptIds)
    {
        foreach (var template in templateChain)
        {
            if (!compositionMap.TryGetValue(template.Id, out var compositions))
                continue;

            foreach (var composition in compositions)
                ResolveComposedAlarmsRecursive(
                    composition, composition.InstanceName,
                    compositionMap, composedTemplateChains, alarms, alarmScriptIds,
                    new HashSet<int>());
        }
    }

    /// <summary>
    /// Recursively resolves the alarms of a composed module and every module
    /// nested inside it, path-qualifying each canonical name with the
    /// accumulated <paramref name="prefix"/>.
    /// </summary>
    private static void ResolveComposedAlarmsRecursive(
        TemplateComposition composition,
        string prefix,
        IReadOnlyDictionary<int, IReadOnlyList<TemplateComposition>> compositionMap,
        IReadOnlyDictionary<int, IReadOnlyList<Template>> composedTemplateChains,
        Dictionary<string, ResolvedAlarm> alarms,
        Dictionary<string, int> alarmScriptIds,
        HashSet<int> path)
    {
        if (!composedTemplateChains.TryGetValue(composition.ComposedTemplateId, out var composedChain))
            return;

        var composedAlarms = ResolveInheritedAlarms(composedChain, prefix, alarmScriptIds);
        foreach (var (name, alarm) in composedAlarms)
        {
            var canonicalName = $"{prefix}.{name}";
            if (!alarms.ContainsKey(canonicalName))
            {
                alarms[canonicalName] = alarm with
                {
                    CanonicalName = canonicalName,
                    TriggerConfiguration = PrefixTriggerAttribute(alarm.TriggerConfiguration, prefix),
                    Source = "Composed"
                };
            }
        }

        // Descend into nested compositions of every template in the chain.
        // Guard on the recursion PATH, not globally: composition is by-slot, so a
        // template legitimately appears twice under different prefixes; only a
        // template already on the CURRENT path is a cycle.
        foreach (var composedTemplate in composedChain)
        {
            if (!path.Add(composedTemplate.Id))
                continue;
            try
            {
                if (!compositionMap.TryGetValue(composedTemplate.Id, out var nestedCompositions))
                    continue;

                foreach (var nested in nestedCompositions)
                    ResolveComposedAlarmsRecursive(
                        nested, $"{prefix}.{nested.InstanceName}",
                        compositionMap, composedTemplateChains, alarms, alarmScriptIds, path);
            }
            finally
            {
                path.Remove(composedTemplate.Id);
            }
        }
    }

    /// <summary>
    /// Resolves native alarm source bindings from an inheritance chain. Keys of
    /// the returned dictionary are bare binding names (caller path-qualifies for
    /// composed modules). Derived templates win unless the base binding is
    /// locked; <see cref="TemplateNativeAlarmSource.IsInherited"/> placeholders
    /// never shadow a live base binding.
    /// </summary>
    private static Dictionary<string, ResolvedNativeAlarmSource> ResolveInheritedNativeAlarmSources(
        IReadOnlyList<Template> templateChain,
        string? prefix)
    {
        var result = new Dictionary<string, ResolvedNativeAlarmSource>(StringComparer.Ordinal);
        // Tracks bindings the base locked, so derived templates cannot override them.
        var lockedNames = new HashSet<string>(StringComparer.Ordinal);

        for (int i = templateChain.Count - 1; i >= 0; i--)
        {
            var template = templateChain[i];
            var source = i == 0 ? "Template" : "Inherited";

            foreach (var binding in template.NativeAlarmSources)
            {
                if (result.ContainsKey(binding.Name))
                {
                    if (lockedNames.Contains(binding.Name))
                        continue;
                    // IsInherited rows on a derived template are placeholders that
                    // must not shadow the live base binding.
                    if (binding.IsInherited)
                        continue;
                }

                result[binding.Name] = new ResolvedNativeAlarmSource
                {
                    CanonicalName = binding.Name,
                    ConnectionName = binding.ConnectionName,
                    SourceReference = binding.SourceReference,
                    ConditionFilter = binding.ConditionFilter,
                    Source = source,
                    IsLocked = binding.IsLocked || lockedNames.Contains(binding.Name)
                };

                if (binding.IsLocked)
                    lockedNames.Add(binding.Name);
            }
        }

        return result;
    }

    private static void ResolveComposedNativeAlarmSources(
        IReadOnlyList<Template> templateChain,
        IReadOnlyDictionary<int, IReadOnlyList<TemplateComposition>> compositionMap,
        IReadOnlyDictionary<int, IReadOnlyList<Template>> composedTemplateChains,
        Dictionary<string, ResolvedNativeAlarmSource> sources)
    {
        foreach (var template in templateChain)
        {
            if (!compositionMap.TryGetValue(template.Id, out var compositions))
                continue;

            foreach (var composition in compositions)
                ResolveComposedNativeAlarmSourcesRecursive(
                    composition, composition.InstanceName,
                    compositionMap, composedTemplateChains, sources, new HashSet<int>());
        }
    }

    /// <summary>
    /// Recursively resolves native alarm source bindings of a composed module
    /// and nested modules, path-qualifying each canonical name with the
    /// accumulated <paramref name="prefix"/>. The source reference is a raw
    /// connection address, so (unlike alarm trigger configs) it is not rewritten.
    /// </summary>
    private static void ResolveComposedNativeAlarmSourcesRecursive(
        TemplateComposition composition,
        string prefix,
        IReadOnlyDictionary<int, IReadOnlyList<TemplateComposition>> compositionMap,
        IReadOnlyDictionary<int, IReadOnlyList<Template>> composedTemplateChains,
        Dictionary<string, ResolvedNativeAlarmSource> sources,
        HashSet<int> path)
    {
        if (!composedTemplateChains.TryGetValue(composition.ComposedTemplateId, out var composedChain))
            return;

        var composedSources = ResolveInheritedNativeAlarmSources(composedChain, prefix);
        foreach (var (name, binding) in composedSources)
        {
            var canonicalName = $"{prefix}.{name}";
            if (!sources.ContainsKey(canonicalName))
            {
                sources[canonicalName] = binding with
                {
                    CanonicalName = canonicalName,
                    Source = "Composed"
                };
            }
        }

        // Guard on the recursion PATH, not globally: composition is by-slot, so a
        // template legitimately appears twice under different prefixes; only a
        // template already on the CURRENT path is a cycle.
        foreach (var composedTemplate in composedChain)
        {
            if (!path.Add(composedTemplate.Id))
                continue;
            try
            {
                if (!compositionMap.TryGetValue(composedTemplate.Id, out var nestedCompositions))
                    continue;

                foreach (var nested in nestedCompositions)
                    ResolveComposedNativeAlarmSourcesRecursive(
                        nested, $"{prefix}.{nested.InstanceName}",
                        compositionMap, composedTemplateChains, sources, path);
            }
            finally
            {
                path.Remove(composedTemplate.Id);
            }
        }
    }

    private static void ApplyInstanceNativeAlarmSourceOverrides(
        ICollection<InstanceNativeAlarmSourceOverride> overrides,
        Dictionary<string, ResolvedNativeAlarmSource> sources)
    {
        foreach (var ovr in overrides)
        {
            if (!sources.TryGetValue(ovr.SourceCanonicalName, out var existing))
                continue; // Cannot add new bindings via overrides.

            if (existing.IsLocked)
                continue; // Locked at the template level — instance override is ignored.

            sources[ovr.SourceCanonicalName] = existing with
            {
                ConnectionName = ovr.ConnectionNameOverride ?? existing.ConnectionName,
                SourceReference = ovr.SourceReferenceOverride ?? existing.SourceReference,
                ConditionFilter = ovr.ConditionFilterOverride ?? existing.ConditionFilter,
                Source = "Override"
            };
        }
    }

    /// <summary>
    /// Resolves scripts from an inheritance chain. The returned dictionary is
    /// keyed by bare script name. <paramref name="scriptCanonicalById"/> is
    /// populated with each resolved script's <see cref="TemplateScript.Id"/>
    /// mapped to the canonical name it will ultimately carry (bare when
    /// <paramref name="prefix"/> is null, otherwise <c>prefix.name</c>).
    /// </summary>
    private static Dictionary<string, ResolvedScript> ResolveInheritedScripts(
        IReadOnlyList<Template> templateChain,
        string? prefix,
        Dictionary<int, string> scriptCanonicalById)
    {
        var result = new Dictionary<string, ResolvedScript>(StringComparer.Ordinal);
        var idByName = new Dictionary<string, int>(StringComparer.Ordinal);

        for (int i = templateChain.Count - 1; i >= 0; i--)
        {
            var template = templateChain[i];
            var source = i == 0 ? "Template" : "Inherited";

            foreach (var script in template.Scripts)
            {
                if (result.TryGetValue(script.Name, out var existing))
                {
                    if (existing.IsLocked)
                        continue;
                    if (script.IsInherited)
                        continue;
                }

                result[script.Name] = new ResolvedScript
                {
                    CanonicalName = script.Name,
                    Code = script.Code,
                    IsLocked = script.IsLocked,
                    TriggerType = script.TriggerType,
                    TriggerConfiguration = script.TriggerConfiguration,
                    ParameterDefinitions = script.ParameterDefinitions,
                    ReturnDefinition = script.ReturnDefinition,
                    MinTimeBetweenRuns = script.MinTimeBetweenRuns,
                    // Per-script timeout rides along on the winning row.
                    // Scripts inherit/override at whole-row granularity (no per-field
                    // merge), so this follows the same rule as the script body/MinTime.
                    ExecutionTimeoutSeconds = script.ExecutionTimeoutSeconds,
                    Source = source
                };
                idByName[script.Name] = script.Id;
            }
        }

        foreach (var (name, id) in idByName)
        {
            if (id == 0) continue; // unsaved row — no stable id to map
            var canonical = prefix == null ? name : $"{prefix}.{name}";
            scriptCanonicalById[id] = canonical;
        }

        return result;
    }

    private static void ResolveComposedScripts(
        IReadOnlyList<Template> templateChain,
        IReadOnlyDictionary<int, IReadOnlyList<TemplateComposition>> compositionMap,
        IReadOnlyDictionary<int, IReadOnlyList<Template>> composedTemplateChains,
        Dictionary<string, ResolvedScript> scripts,
        Dictionary<int, string> scriptCanonicalById)
    {
        foreach (var template in templateChain)
        {
            if (!compositionMap.TryGetValue(template.Id, out var compositions))
                continue;

            foreach (var composition in compositions)
                ResolveComposedScriptsRecursive(
                    composition, composition.InstanceName, parentPath: "",
                    compositionMap, composedTemplateChains, scripts, scriptCanonicalById,
                    new HashSet<int>());
        }
    }

    /// <summary>
    /// Recursively resolves the scripts of a composed module and every module
    /// nested inside it, path-qualifying each canonical name with the
    /// accumulated <paramref name="prefix"/>. <paramref name="parentPath"/> is
    /// the path of the enclosing module — empty for a top-level composition
    /// (nesting depth 1; parent is the root template) and the enclosing module's
    /// <c>prefix</c> for deeper nesting — and is carried into each script's
    /// <see cref="ScriptScope"/> so a nested script's <c>Parent.X</c>
    /// resolves against its real parent module.
    /// </summary>
    private static void ResolveComposedScriptsRecursive(
        TemplateComposition composition,
        string prefix,
        string parentPath,
        IReadOnlyDictionary<int, IReadOnlyList<TemplateComposition>> compositionMap,
        IReadOnlyDictionary<int, IReadOnlyList<Template>> composedTemplateChains,
        Dictionary<string, ResolvedScript> scripts,
        Dictionary<int, string> scriptCanonicalById,
        HashSet<int> path)
    {
        if (!composedTemplateChains.TryGetValue(composition.ComposedTemplateId, out var composedChain))
            return;

        var composedScripts = ResolveInheritedScripts(composedChain, prefix, scriptCanonicalById);
        foreach (var (name, script) in composedScripts)
        {
            var canonicalName = $"{prefix}.{name}";
            if (!scripts.ContainsKey(canonicalName))
            {
                scripts[canonicalName] = script with
                {
                    CanonicalName = canonicalName,
                    Source = "Composed",
                    Scope = new Commons.Types.Scripts.ScriptScope(SelfPath: prefix, ParentPath: parentPath)
                };
            }
        }

        // Descend into nested compositions of every template in the chain.
        // Guard on the recursion PATH, not globally: composition is by-slot, so a
        // template legitimately appears twice under different prefixes; only a
        // template already on the CURRENT path is a cycle.
        foreach (var composedTemplate in composedChain)
        {
            if (!path.Add(composedTemplate.Id))
                continue;
            try
            {
                if (!compositionMap.TryGetValue(composedTemplate.Id, out var nestedCompositions))
                    continue;

                foreach (var nested in nestedCompositions)
                    ResolveComposedScriptsRecursive(
                        nested, $"{prefix}.{nested.InstanceName}", parentPath: prefix,
                        compositionMap, composedTemplateChains, scripts, scriptCanonicalById, path);
            }
            finally
            {
                path.Remove(composedTemplate.Id);
            }
        }
    }

    /// <summary>
    /// Prefixes the "attribute" (or "attributeName") field in alarm trigger configuration JSON
    /// with the composition instance name, so composed alarms monitor the path-qualified attribute.
    /// </summary>
    private static string? PrefixTriggerAttribute(string? triggerConfigJson, string prefix)
    {
        if (string.IsNullOrEmpty(triggerConfigJson)) return triggerConfigJson;

        try
        {
            using var doc = System.Text.Json.JsonDocument.Parse(triggerConfigJson);
            var root = doc.RootElement;

            // Find the attribute key name used
            string? attrKey = null;
            if (root.TryGetProperty("attribute", out _)) attrKey = "attribute";
            else if (root.TryGetProperty("attributeName", out _)) attrKey = "attributeName";

            if (attrKey == null) return triggerConfigJson;

            var attrValue = root.GetProperty(attrKey).GetString();
            if (string.IsNullOrEmpty(attrValue)) return triggerConfigJson;

            // Rebuild JSON with prefixed attribute name
            using var ms = new System.IO.MemoryStream();
            using (var writer = new System.Text.Json.Utf8JsonWriter(ms))
            {
                writer.WriteStartObject();
                foreach (var prop in root.EnumerateObject())
                {
                    if (prop.Name == attrKey)
                        writer.WriteString(attrKey, $"{prefix}.{attrValue}");
                    else
                        prop.WriteTo(writer);
                }
                writer.WriteEndObject();
            }
            return System.Text.Encoding.UTF8.GetString(ms.ToArray());
        }
        catch
        {
            return triggerConfigJson;
        }
    }

    /// <summary>
    /// Resolves alarm on-trigger script references from <see cref="TemplateScript.Id"/>
    /// values to the canonical (path-qualified) names of the corresponding
    /// resolved scripts. <paramref name="alarmScriptIds"/> maps an alarm's
    /// canonical name to the id of its on-trigger script; <paramref name="scriptCanonicalById"/>
    /// maps a script id to the canonical name it carries in the flattened
    /// configuration. An alarm whose on-trigger script id has no matching
    /// resolved script is left with a <c>null</c> reference — semantic
    /// validation then reports the dangling reference.
    /// </summary>
    private static void ResolveAlarmScriptReferences(
        Dictionary<string, ResolvedAlarm> alarms,
        Dictionary<string, int> alarmScriptIds,
        Dictionary<int, string> scriptCanonicalById)
    {
        foreach (var (alarmCanonicalName, scriptId) in alarmScriptIds)
        {
            if (!alarms.TryGetValue(alarmCanonicalName, out var alarm))
                continue;

            scriptCanonicalById.TryGetValue(scriptId, out var scriptCanonicalName);
            alarms[alarmCanonicalName] = alarm with
            {
                OnTriggerScriptCanonicalName = scriptCanonicalName
            };
        }
    }
}
