using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Configuration.Enums;

namespace ZB.MOM.WW.OtOpcUa.OpcUaServer;

/// <summary>Outcome of <see cref="Phase7Composer.Compose"/> — pure value tuple, no side effects.
/// <see cref="UnsAreas"/> + <see cref="UnsLines"/> carry the UNS topology so the applier can
/// materialise the Area/Line/Equipment folder hierarchy in the address space; equipment carries
/// its parent line id so the applier knows where to hang each equipment folder.
/// <see cref="GalaxyTags"/> carries SystemPlatform-namespace tags (Galaxy hierarchy) so the
/// applier can materialise their FolderPath + Variable nodes ahead of any driver subscribe.</summary>
public sealed record Phase7CompositionResult(
    IReadOnlyList<UnsAreaProjection> UnsAreas,
    IReadOnlyList<UnsLineProjection> UnsLines,
    IReadOnlyList<EquipmentNode> EquipmentNodes,
    IReadOnlyList<DriverInstancePlan> DriverInstancePlans,
    IReadOnlyList<ScriptedAlarmPlan> ScriptedAlarmPlans,
    IReadOnlyList<GalaxyTagPlan> GalaxyTags)
{
    /// <summary>Convenience constructor for tests + earlier callers that don't carry UNS or Galaxy data.</summary>
    /// <param name="equipmentNodes">The equipment nodes.</param>
    /// <param name="driverInstancePlans">The driver instance plans.</param>
    /// <param name="scriptedAlarmPlans">The scripted alarm plans.</param>
    public Phase7CompositionResult(
        IReadOnlyList<EquipmentNode> equipmentNodes,
        IReadOnlyList<DriverInstancePlan> driverInstancePlans,
        IReadOnlyList<ScriptedAlarmPlan> scriptedAlarmPlans)
        : this(Array.Empty<UnsAreaProjection>(), Array.Empty<UnsLineProjection>(),
               equipmentNodes, driverInstancePlans, scriptedAlarmPlans, Array.Empty<GalaxyTagPlan>())
    {
    }

    /// <summary>Convenience constructor for callers carrying UNS but not Galaxy data.</summary>
    /// <param name="unsAreas">The UNS areas.</param>
    /// <param name="unsLines">The UNS lines.</param>
    /// <param name="equipmentNodes">The equipment nodes.</param>
    /// <param name="driverInstancePlans">The driver instance plans.</param>
    /// <param name="scriptedAlarmPlans">The scripted alarm plans.</param>
    public Phase7CompositionResult(
        IReadOnlyList<UnsAreaProjection> unsAreas,
        IReadOnlyList<UnsLineProjection> unsLines,
        IReadOnlyList<EquipmentNode> equipmentNodes,
        IReadOnlyList<DriverInstancePlan> driverInstancePlans,
        IReadOnlyList<ScriptedAlarmPlan> scriptedAlarmPlans)
        : this(unsAreas, unsLines, equipmentNodes, driverInstancePlans, scriptedAlarmPlans, Array.Empty<GalaxyTagPlan>())
    {
    }
}

public sealed record UnsAreaProjection(string UnsAreaId, string DisplayName);
public sealed record UnsLineProjection(string UnsLineId, string UnsAreaId, string DisplayName);
public sealed record EquipmentNode(string EquipmentId, string DisplayName, string UnsLineId);
public sealed record DriverInstancePlan(string DriverInstanceId, string DriverType, string ConfigJson);
public sealed record ScriptedAlarmPlan(string ScriptedAlarmId, string EquipmentId, string PredicateScriptId, string MessageTemplate);

/// <summary>
///     One Galaxy / SystemPlatform-namespace tag from a <see cref="Tag"/> row where
///     <see cref="Tag.EquipmentId"/> is null. Carries the FolderPath segment that the applier
///     turns into a folder, the leaf <see cref="DisplayName"/> for the Variable, the OPC UA
///     <see cref="DataType"/>, and the dot-form MXAccess reference (<see cref="MxAccessRef"/>)
///     that the Galaxy driver consumes when subscribing.
/// </summary>
public sealed record GalaxyTagPlan(
    string TagId,
    string DriverInstanceId,
    string FolderPath,
    string DisplayName,
    string DataType,
    string MxAccessRef);

/// <summary>
/// Pure composer that flattens the live-edit DB tables into the address-space build plan a
/// driver-role host needs. Same inputs → same outputs, no logging, no DB writes. The driver-role
/// startup (Task 53) consumes the result and hands it to the node-manager factory.
///
/// #85 — the composer now carries UNS topology (<see cref="UnsAreaProjection"/> +
/// <see cref="UnsLineProjection"/>) so <c>Phase7Applier</c> can build the
/// <c>Area/Line/Equipment</c> folder hierarchy in the SDK's address space. The legacy
/// <c>EquipmentNodeWalker</c> integration that did this server-side is fully replaced by the
/// (composer → applier → sink → node manager) chain.
/// </summary>
public static class Phase7Composer
{
    /// <summary>Convenience overload for legacy callers + tests that don't yet supply UNS / Galaxy data.</summary>
    /// <param name="equipment">The equipment.</param>
    /// <param name="driverInstances">The driver instances.</param>
    /// <param name="scriptedAlarms">The scripted alarms.</param>
    /// <returns>The composition result.</returns>
    public static Phase7CompositionResult Compose(
        IReadOnlyList<Equipment> equipment,
        IReadOnlyList<DriverInstance> driverInstances,
        IReadOnlyList<ScriptedAlarm> scriptedAlarms) =>
        Compose(Array.Empty<UnsArea>(), Array.Empty<UnsLine>(), equipment, driverInstances, scriptedAlarms,
                Array.Empty<Tag>(), Array.Empty<Namespace>());

    /// <summary>UNS-aware overload that doesn't yet supply Galaxy tags.</summary>
    /// <param name="unsAreas">The UNS areas.</param>
    /// <param name="unsLines">The UNS lines.</param>
    /// <param name="equipment">The equipment.</param>
    /// <param name="driverInstances">The driver instances.</param>
    /// <param name="scriptedAlarms">The scripted alarms.</param>
    /// <returns>The composition result.</returns>
    public static Phase7CompositionResult Compose(
        IReadOnlyList<UnsArea> unsAreas,
        IReadOnlyList<UnsLine> unsLines,
        IReadOnlyList<Equipment> equipment,
        IReadOnlyList<DriverInstance> driverInstances,
        IReadOnlyList<ScriptedAlarm> scriptedAlarms) =>
        Compose(unsAreas, unsLines, equipment, driverInstances, scriptedAlarms,
                Array.Empty<Tag>(), Array.Empty<Namespace>());

    /// <summary>
    ///     Composes the address space build plan from the configuration entities.
    /// </summary>
    /// <param name="unsAreas">The UNS areas.</param>
    /// <param name="unsLines">The UNS lines.</param>
    /// <param name="equipment">The equipment.</param>
    /// <param name="driverInstances">The driver instances.</param>
    /// <param name="scriptedAlarms">The scripted alarms.</param>
    /// <param name="tags">The tags.</param>
    /// <param name="namespaces">The namespaces.</param>
    /// <returns>The composition result.</returns>
    public static Phase7CompositionResult Compose(
        IReadOnlyList<UnsArea> unsAreas,
        IReadOnlyList<UnsLine> unsLines,
        IReadOnlyList<Equipment> equipment,
        IReadOnlyList<DriverInstance> driverInstances,
        IReadOnlyList<ScriptedAlarm> scriptedAlarms,
        IReadOnlyList<Tag> tags,
        IReadOnlyList<Namespace> namespaces)
    {
        var areas = unsAreas
            .OrderBy(a => a.UnsAreaId, StringComparer.Ordinal)
            .Select(a => new UnsAreaProjection(a.UnsAreaId, a.Name))
            .ToList();

        var lines = unsLines
            .OrderBy(l => l.UnsLineId, StringComparer.Ordinal)
            .Select(l => new UnsLineProjection(l.UnsLineId, l.UnsAreaId, l.Name))
            .ToList();

        var nodes = equipment
            .OrderBy(e => e.EquipmentId, StringComparer.Ordinal)
            .Select(e => new EquipmentNode(e.EquipmentId, e.MachineCode, e.UnsLineId))
            .ToList();

        var plans = driverInstances
            .OrderBy(d => d.DriverInstanceId, StringComparer.Ordinal)
            .Select(d => new DriverInstancePlan(d.DriverInstanceId, d.DriverType, d.DriverConfig))
            .ToList();

        var alarms = scriptedAlarms
            .OrderBy(a => a.ScriptedAlarmId, StringComparer.Ordinal)
            .Select(a => new ScriptedAlarmPlan(a.ScriptedAlarmId, a.EquipmentId, a.PredicateScriptId, a.MessageTemplate))
            .ToList();

        // SystemPlatform tags = Galaxy tags. Match each tag to its DriverInstance and that
        // driver's Namespace; emit only when the namespace kind is SystemPlatform AND the tag
        // has no EquipmentId (per the entity invariant for SystemPlatform).
        var driversById = driverInstances.ToDictionary(d => d.DriverInstanceId, StringComparer.Ordinal);
        var namespacesById = namespaces.ToDictionary(n => n.NamespaceId, StringComparer.Ordinal);

        var galaxyTags = tags
            .Where(t => t.EquipmentId is null)
            .Where(t => driversById.TryGetValue(t.DriverInstanceId, out var di)
                        && namespacesById.TryGetValue(di.NamespaceId, out var ns)
                        && ns.Kind == NamespaceKind.SystemPlatform)
            .OrderBy(t => t.DriverInstanceId, StringComparer.Ordinal)
            .ThenBy(t => t.FolderPath, StringComparer.Ordinal)
            .ThenBy(t => t.Name, StringComparer.Ordinal)
            .Select(t => new GalaxyTagPlan(
                TagId: t.TagId,
                DriverInstanceId: t.DriverInstanceId,
                FolderPath: t.FolderPath ?? string.Empty,
                DisplayName: t.Name,
                DataType: t.DataType,
                // MXAccess reference: "FolderPath.Name" when FolderPath is set, else just "Name".
                MxAccessRef: string.IsNullOrWhiteSpace(t.FolderPath) ? t.Name : $"{t.FolderPath}.{t.Name}"))
            .ToList();

        return new Phase7CompositionResult(areas, lines, nodes, plans, alarms, galaxyTags);
    }
}
