using Microsoft.EntityFrameworkCore;
using ZB.MOM.WW.OtOpcUa.Configuration;
using ZB.MOM.WW.OtOpcUa.Configuration.Entities;
using ZB.MOM.WW.OtOpcUa.Core.OpcUa;

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

/// <summary>
///     Loads the <see cref="EquipmentNamespaceContent"/> snapshot the
///     <see cref="EquipmentNodeWalker"/> consumes, scoped to a single
///     (driverInstanceId, generationId) pair. Joins the four row sets the walker expects:
///     UnsAreas for the driver's cluster, UnsLines under those areas, Equipment bound to
///     this driver + its lines, and Tags bound to this driver + its equipment — all at the
///     supplied generation.
/// </summary>
/// <remarks>
///     <para>The walker is driver-instance-scoped (decisions #116–#121 put the UNS in the
///     Equipment-kind namespace owned by one driver instance at a time), so this loader is
///     too — a single call returns one driver's worth of rows, never the whole fleet.</para>
///
///     <para>Returns <c>null</c> when the driver instance has no Equipment rows at the
///     supplied generation. The wire-in in <see cref="OpcUaApplicationHost"/> treats null as
///     "this driver has no UNS content, skip the walker and let DiscoverAsync own the whole
///     address space" — the backward-compat path for drivers whose namespace kind is not
///     Equipment (Modbus / AB CIP / TwinCAT / FOCAS).</para>
/// </remarks>
public sealed class EquipmentNamespaceContentLoader
{
    private readonly OtOpcUaConfigDbContext _db;

    public EquipmentNamespaceContentLoader(OtOpcUaConfigDbContext db)
    {
        _db = db;
    }

    /// <summary>
    ///     Load the walker-shaped snapshot for <paramref name="driverInstanceId"/> at
    ///     <paramref name="generationId"/>. Returns <c>null</c> when the driver has no
    ///     Equipment rows at that generation.
    /// </summary>
    public async Task<EquipmentNamespaceContent?> LoadAsync(
        string driverInstanceId, long generationId, CancellationToken ct)
    {
        var equipment = await _db.Equipment
            .AsNoTracking()
            .Where(e => e.DriverInstanceId == driverInstanceId && e.GenerationId == generationId && e.Enabled)
            .ToListAsync(ct).ConfigureAwait(false);

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

        // Filter UNS tree to only the lines + areas that host at least one Equipment bound to
        // this driver — skips loading unrelated UNS branches from the cluster. LinesByArea
        // grouping is driven off the Equipment rows so an empty line (no equipment) doesn't
        // pull a pointless folder into the walker output.
        var lineIds = equipment.Select(e => e.UnsLineId).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();

        var lines = await _db.UnsLines
            .AsNoTracking()
            .Where(l => l.GenerationId == generationId && lineIds.Contains(l.UnsLineId))
            .ToListAsync(ct).ConfigureAwait(false);

        var areaIds = lines.Select(l => l.UnsAreaId).Distinct(StringComparer.OrdinalIgnoreCase).ToArray();

        var areas = await _db.UnsAreas
            .AsNoTracking()
            .Where(a => a.GenerationId == generationId && areaIds.Contains(a.UnsAreaId))
            .ToListAsync(ct).ConfigureAwait(false);

        // Tags belonging to this driver at this generation. Walker skips Tags with null
        // EquipmentId (those are SystemPlatform-kind Galaxy tags per decision #120) but we
        // load them anyway so the same rowset can drive future non-Equipment-kind walks
        // without re-hitting the DB. Filtering here is a future optimization; today the
        // per-tag cost is bounded by driver scope.
        var tags = await _db.Tags
            .AsNoTracking()
            .Where(t => t.DriverInstanceId == driverInstanceId && t.GenerationId == generationId)
            .ToListAsync(ct).ConfigureAwait(false);

        return new EquipmentNamespaceContent(
            Areas: areas,
            Lines: lines,
            Equipment: equipment,
            Tags: tags);
    }
}
