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

namespace ZB.MOM.WW.OtOpcUa.Server.Redundancy;

/// <summary>
///     Pure-function mapper from the shared config DB's <see cref="ServerCluster"/> +
///     <see cref="ClusterNode"/> rows to an immutable <see cref="RedundancyTopology"/>.
///     Validates Phase 6.3 Stream A.1 invariants and throws
///     <see cref="InvalidTopologyException"/> on violation so the coordinator can fail startup
///     fast with a clear message rather than boot into an ambiguous state.
/// </summary>
/// <remarks>
///     Stateless — the caller owns the DB round-trip + hands rows in. Keeping it pure makes
///     the invariant matrix testable without EF or SQL Server.
/// </remarks>
public static class ClusterTopologyLoader
{
    /// <summary>Build a topology snapshot for the given self node. Throws on invariant violation.</summary>
    public static RedundancyTopology Load(string selfNodeId, ServerCluster cluster, IReadOnlyList<ClusterNode> nodes)
    {
        ArgumentException.ThrowIfNullOrWhiteSpace(selfNodeId);
        ArgumentNullException.ThrowIfNull(cluster);
        ArgumentNullException.ThrowIfNull(nodes);

        ValidateClusterShape(cluster, nodes);
        ValidateUniqueApplicationUris(nodes);
        ValidatePrimaryCount(cluster, nodes);

        var self = nodes.FirstOrDefault(n => string.Equals(n.NodeId, selfNodeId, StringComparison.OrdinalIgnoreCase))
                   ?? throw new InvalidTopologyException(
                       $"Self node '{selfNodeId}' is not a member of cluster '{cluster.ClusterId}'. " +
                       $"Members: {string.Join(", ", nodes.Select(n => n.NodeId))}.");

        var peers = nodes
            .Where(n => !string.Equals(n.NodeId, selfNodeId, StringComparison.OrdinalIgnoreCase))
            .Select(n => new RedundancyPeer(
                NodeId: n.NodeId,
                Role: n.RedundancyRole,
                Host: n.Host,
                OpcUaPort: n.OpcUaPort,
                DashboardPort: n.DashboardPort,
                ApplicationUri: n.ApplicationUri))
            .ToList();

        return new RedundancyTopology(
            ClusterId: cluster.ClusterId,
            SelfNodeId: self.NodeId,
            SelfRole: self.RedundancyRole,
            Mode: cluster.RedundancyMode,
            Peers: peers,
            SelfApplicationUri: self.ApplicationUri);
    }

    private static void ValidateClusterShape(ServerCluster cluster, IReadOnlyList<ClusterNode> nodes)
    {
        if (nodes.Count == 0)
            throw new InvalidTopologyException($"Cluster '{cluster.ClusterId}' has zero nodes.");

        // Decision #83 — v2.0 caps clusters at two nodes.
        if (nodes.Count > 2)
            throw new InvalidTopologyException(
                $"Cluster '{cluster.ClusterId}' has {nodes.Count} nodes. v2.0 supports at most 2 nodes per cluster (decision #83).");

        // Every node must belong to the given cluster.
        var wrongCluster = nodes.FirstOrDefault(n =>
            !string.Equals(n.ClusterId, cluster.ClusterId, StringComparison.OrdinalIgnoreCase));
        if (wrongCluster is not null)
            throw new InvalidTopologyException(
                $"Node '{wrongCluster.NodeId}' belongs to cluster '{wrongCluster.ClusterId}', not '{cluster.ClusterId}'.");
    }

    private static void ValidateUniqueApplicationUris(IReadOnlyList<ClusterNode> nodes)
    {
        var dup = nodes
            .GroupBy(n => n.ApplicationUri, StringComparer.Ordinal)
            .FirstOrDefault(g => g.Count() > 1);
        if (dup is not null)
            throw new InvalidTopologyException(
                $"Nodes {string.Join(", ", dup.Select(n => n.NodeId))} share ApplicationUri '{dup.Key}'. " +
                $"OPC UA Part 4 requires unique ApplicationUri per server — clients pin trust here (decision #86).");
    }

    private static void ValidatePrimaryCount(ServerCluster cluster, IReadOnlyList<ClusterNode> nodes)
    {
        // Standalone mode: any role is fine. Warm / Hot: at most one Primary per cluster.
        if (cluster.RedundancyMode == RedundancyMode.None) return;

        var primaries = nodes.Count(n => n.RedundancyRole == RedundancyRole.Primary);
        if (primaries > 1)
            throw new InvalidTopologyException(
                $"Cluster '{cluster.ClusterId}' has {primaries} Primary nodes in redundancy mode {cluster.RedundancyMode}. " +
                $"At most one Primary per cluster (decision #84). Runtime detects and demotes both to ServiceLevel 2 " +
                $"per the 8-state matrix; startup fails fast to surface the misconfiguration earlier.");
    }
}
