using ScadaLink.Commons.Entities.Instances;
using ScadaLink.Commons.Entities.Sites;
using ScadaLink.Commons.Entities.Templates;
using ScadaLink.Commons.Types;
using ScadaLink.Commons.Types.Enums;
using ScadaLink.Commons.Types.Flattening;

namespace ScadaLink.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 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
            var alarms = ResolveInheritedAlarms(templateChain);
            ResolveComposedAlarms(templateChain, compositionMap, composedTemplateChains, alarms);

            // Step 6: Resolve scripts from inheritance chain
            var scripts = ResolveInheritedScripts(templateChain);
            ResolveComposedScripts(templateChain, compositionMap, composedTemplateChains, scripts);

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

            // 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(),
                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
        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 a parent defined this attribute as locked, derived cannot change the value
                if (result.TryGetValue(attr.Name, out var existing) && existing.IsLocked)
                    continue;

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

        return result;
    }

    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)
            {
                if (!composedTemplateChains.TryGetValue(composition.ComposedTemplateId, out var composedChain))
                    continue;

                var prefix = composition.InstanceName;
                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"
                        };
                    }
                }

                // Recurse into nested compositions
                foreach (var composedTemplate in composedChain)
                {
                    if (!compositionMap.TryGetValue(composedTemplate.Id, out var nestedCompositions))
                        continue;

                    foreach (var nested in nestedCompositions)
                    {
                        if (!composedTemplateChains.TryGetValue(nested.ComposedTemplateId, out var nestedChain))
                            continue;

                        var nestedPrefix = $"{prefix}.{nested.InstanceName}";
                        var nestedAttrs = ResolveInheritedAttributes(nestedChain);

                        foreach (var (name, attr) in nestedAttrs)
                        {
                            var canonicalName = $"{nestedPrefix}.{name}";
                            if (!attributes.ContainsKey(canonicalName))
                            {
                                attributes[canonicalName] = attr with
                                {
                                    CanonicalName = canonicalName,
                                    Source = "Composed"
                                };
                            }
                        }
                    }
                }
            }
        }
    }

    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"
            };
        }
    }

    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
            };
        }
    }

    private static Dictionary<string, ResolvedAlarm> ResolveInheritedAlarms(
        IReadOnlyList<Template> templateChain)
    {
        var result = new Dictionary<string, ResolvedAlarm>(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) && existing.IsLocked)
                    continue;

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

        return result;
    }

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

            foreach (var composition in compositions)
            {
                if (!composedTemplateChains.TryGetValue(composition.ComposedTemplateId, out var composedChain))
                    continue;

                var prefix = composition.InstanceName;
                var composedAlarms = ResolveInheritedAlarms(composedChain);

                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"
                        };
                    }
                }
            }
        }
    }

    private static Dictionary<string, ResolvedScript> ResolveInheritedScripts(
        IReadOnlyList<Template> templateChain)
    {
        var result = new Dictionary<string, ResolvedScript>(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) && existing.IsLocked)
                    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,
                    Source = source
                };
            }
        }

        return result;
    }

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

            foreach (var composition in compositions)
            {
                if (!composedTemplateChains.TryGetValue(composition.ComposedTemplateId, out var composedChain))
                    continue;

                var prefix = composition.InstanceName;
                var composedScripts = ResolveInheritedScripts(composedChain);

                foreach (var (name, script) in composedScripts)
                {
                    var canonicalName = $"{prefix}.{name}";
                    if (!scripts.ContainsKey(canonicalName))
                    {
                        scripts[canonicalName] = script with
                        {
                            CanonicalName = canonicalName,
                            Source = "Composed"
                        };
                    }
                }
            }
        }
    }

    /// <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 script IDs to canonical names.
    /// This is done by finding the script in the template chain whose ID matches the alarm's OnTriggerScriptId,
    /// then mapping to the corresponding canonical name in the resolved scripts.
    /// </summary>
    private static void ResolveAlarmScriptReferences(
        Dictionary<string, ResolvedAlarm> alarms,
        Dictionary<string, ResolvedScript> scripts)
    {
        // Build a lookup of script names (we only have canonical names at this point)
        // The alarm's OnTriggerScriptCanonicalName will be set by the caller or validation step
        // For now, this is a placeholder — the actual resolution depends on how alarm trigger configs
        // reference scripts (by name within the same scope).
        // The trigger configuration JSON may contain a "scriptName" field.
    }
}
