using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.OtOpcUa.Configuration;

namespace ZB.MOM.WW.OtOpcUa.AdminUI.Uns;

/// <summary>
///     Default <see cref="IEffectiveNameGuard"/>. Loads the owning equipment's current effective-name
///     set — references (<c>DisplayNameOverride</c> else the backing raw tag's <c>Name</c>),
///     VirtualTags (<c>Name</c>), and ScriptedAlarms (<c>Name</c>) — and reports the first collision
///     with a proposed name. Comparison is <see cref="StringComparer.Ordinal"/> to match the
///     <c>{EquipmentId}/{EffectiveName}</c> NodeId semantics and the deploy-time
///     <c>DraftValidator</c> <c>UnsEffectiveNameCollision</c> rule.
/// </summary>
/// <remarks>
///     Each call creates and disposes its own pooled context — the same per-call pattern
///     <see cref="UnsTreeService"/> uses — so the guard is safe to register <c>Scoped</c>.
/// </remarks>
public sealed class EffectiveNameGuard(IDbContextFactory<OtOpcUaConfigDbContext> dbFactory) : IEffectiveNameGuard
{
    /// <inheritdoc />
    public async Task<string?> CheckAsync(
        string equipmentId,
        string proposedEffectiveName,
        EffectiveNameSourceKind kind,
        string? excludeSourceId,
        CancellationToken ct = default)
    {
        await using var db = await dbFactory.CreateDbContextAsync(ct);

        // References for this equipment. The effective name is the display-name override else the
        // backing raw tag's current Name, so the two sets (references + tags) are loaded and joined
        // in memory — the per-equipment set is small and this mirrors the deploy rule exactly
        // (a reference whose backing tag is missing contributes no effective name, so it is skipped).
        var references = await db.UnsTagReferences
            .AsNoTracking()
            .Where(r => r.EquipmentId == equipmentId)
            .Select(r => new { r.UnsTagReferenceId, r.TagId, r.DisplayNameOverride })
            .ToListAsync(ct);

        var tagIds = references.Select(r => r.TagId).Distinct(StringComparer.Ordinal).ToList();
        var tagNameById = (await db.Tags
                .AsNoTracking()
                .Where(t => tagIds.Contains(t.TagId))
                .Select(t => new { t.TagId, t.Name })
                .ToListAsync(ct))
            .GroupBy(t => t.TagId, StringComparer.Ordinal)
            .ToDictionary(g => g.Key, g => g.First().Name, StringComparer.Ordinal);

        foreach (var r in references)
        {
            if (IsSelf(EffectiveNameSourceKind.Reference, r.UnsTagReferenceId, kind, excludeSourceId))
                continue;
            var effective = r.DisplayNameOverride
                ?? (tagNameById.TryGetValue(r.TagId, out var n) ? n : null);
            if (Collides(effective, proposedEffectiveName))
                return Message(proposedEffectiveName, "reference", r.UnsTagReferenceId, equipmentId);
        }

        var virtualTags = await db.VirtualTags
            .AsNoTracking()
            .Where(v => v.EquipmentId == equipmentId)
            .Select(v => new { v.VirtualTagId, v.Name })
            .ToListAsync(ct);

        foreach (var v in virtualTags)
        {
            if (IsSelf(EffectiveNameSourceKind.VirtualTag, v.VirtualTagId, kind, excludeSourceId))
                continue;
            if (Collides(v.Name, proposedEffectiveName))
                return Message(proposedEffectiveName, "VirtualTag", v.VirtualTagId, equipmentId);
        }

        var scriptedAlarms = await db.ScriptedAlarms
            .AsNoTracking()
            .Where(a => a.EquipmentId == equipmentId)
            .Select(a => new { a.ScriptedAlarmId, a.Name })
            .ToListAsync(ct);

        foreach (var a in scriptedAlarms)
        {
            if (IsSelf(EffectiveNameSourceKind.ScriptedAlarm, a.ScriptedAlarmId, kind, excludeSourceId))
                continue;
            if (Collides(a.Name, proposedEffectiveName))
                return Message(proposedEffectiveName, "ScriptedAlarm", a.ScriptedAlarmId, equipmentId);
        }

        return null;
    }

    /// <summary>True when a row is the self-row being edited — same kind AND same logical id — so it
    /// is excluded from the collision set (a rename / override-change of a row cannot collide with itself).</summary>
    private static bool IsSelf(
        EffectiveNameSourceKind rowKind,
        string rowId,
        EffectiveNameSourceKind editedKind,
        string? excludeSourceId) =>
        excludeSourceId is not null
        && rowKind == editedKind
        && string.Equals(rowId, excludeSourceId, StringComparison.Ordinal);

    private static bool Collides(string? existing, string proposed) =>
        existing is not null && string.Equals(existing, proposed, StringComparison.Ordinal);

    private static string Message(string name, string sourceLabel, string sourceId, string equipmentId) =>
        $"effective name '{name}' already used by {sourceLabel} '{sourceId}' in equipment '{equipmentId}'";
}
