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

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

/// <summary>
/// Read-only enumeration helper for the WP5 CSV export path. Reads a device's tag-groups + tags directly
/// from the config DB and flattens them (with each tag's reconstructed <c>/</c>-separated
/// <see cref="RawCsvExportTag.TagGroupPath"/>) into the export shape. Owned by WP5 — it does NOT extend
/// <see cref="IRawTreeService"/> (that prelude is closed); it takes its own
/// <see cref="IDbContextFactory{TContext}"/> so export never mutates. Also resolves a tag-group's path +
/// owning device (the import modal's target-group prefix seam).
/// </summary>
/// <param name="dbFactory">The config-DB context factory.</param>
public sealed class RawTagCsvExportReader(IDbContextFactory<OtOpcUaConfigDbContext> dbFactory)
{
    /// <summary>
    /// The owning device + reconstructed path for a tag-group (used by the import modal to prepend a
    /// target-group prefix to every imported row).
    /// </summary>
    /// <param name="DeviceId">The group's owning device id.</param>
    /// <param name="GroupPath">The group's <c>/</c>-separated path under the device (device-relative, no device segment).</param>
    public readonly record struct TagGroupContext(string DeviceId, string GroupPath);

    /// <summary>Resolves the driver-type string of the device's parent driver, or null when unresolved.</summary>
    /// <param name="deviceId">The device's logical id.</param>
    /// <param name="ct">A cancellation token.</param>
    /// <returns>The driver-type string, or null.</returns>
    public async Task<string?> ResolveDriverTypeAsync(string deviceId, CancellationToken ct = default)
    {
        await using var db = await dbFactory.CreateDbContextAsync(ct);
        return await (from dev in db.Devices.AsNoTracking()
                      join drv in db.DriverInstances.AsNoTracking() on dev.DriverInstanceId equals drv.DriverInstanceId
                      where dev.DeviceId == deviceId
                      select drv.DriverType).FirstOrDefaultAsync(ct);
    }

    /// <summary>
    /// Resolves a tag-group's owning device + device-relative path. Walks the <c>ParentTagGroupId</c> chain
    /// to the device root and joins the segment names with <c>/</c>. Returns null when the group is unknown.
    /// </summary>
    /// <param name="tagGroupId">The tag-group's logical id.</param>
    /// <param name="ct">A cancellation token.</param>
    /// <returns>The owning device + reconstructed path, or null when the group does not exist.</returns>
    public async Task<TagGroupContext?> ResolveGroupContextAsync(string tagGroupId, CancellationToken ct = default)
    {
        await using var db = await dbFactory.CreateDbContextAsync(ct);
        var group = await db.TagGroups.AsNoTracking().FirstOrDefaultAsync(g => g.TagGroupId == tagGroupId, ct);
        if (group is null)
        {
            return null;
        }

        var byId = await db.TagGroups.AsNoTracking()
            .Where(g => g.DeviceId == group.DeviceId)
            .ToDictionaryAsync(g => g.TagGroupId, g => (g.Name, g.ParentTagGroupId), ct);

        var path = BuildGroupPath(tagGroupId, byId);
        return new TagGroupContext(group.DeviceId, path);
    }

    /// <summary>
    /// Enumerates every tag under a device (across all nested tag-groups), each carrying its
    /// device-relative group path, in a stable order (group path, then name).
    /// </summary>
    /// <param name="deviceId">The device's logical id.</param>
    /// <param name="ct">A cancellation token.</param>
    /// <returns>The device's tags flattened for export.</returns>
    public async Task<IReadOnlyList<RawCsvExportTag>> ReadDeviceTagsAsync(string deviceId, CancellationToken ct = default)
    {
        await using var db = await dbFactory.CreateDbContextAsync(ct);

        var groups = await db.TagGroups.AsNoTracking()
            .Where(g => g.DeviceId == deviceId)
            .ToDictionaryAsync(g => g.TagGroupId, g => (g.Name, g.ParentTagGroupId), ct);

        var tags = await db.Tags.AsNoTracking()
            .Where(t => t.DeviceId == deviceId)
            .ToListAsync(ct);

        return tags
            .Select(t => new RawCsvExportTag(
                TagGroupPath: t.TagGroupId is null ? null : NullIfEmpty(BuildGroupPath(t.TagGroupId, groups)),
                Name: t.Name,
                DataType: t.DataType,
                AccessLevel: t.AccessLevel,
                WriteIdempotent: t.WriteIdempotent,
                PollGroupId: t.PollGroupId,
                TagConfig: t.TagConfig))
            .OrderBy(x => x.TagGroupPath ?? "", RawPaths.Comparer)
            .ThenBy(x => x.Name, RawPaths.Comparer)
            .ToList();
    }

    private static string? NullIfEmpty(string s) => s.Length == 0 ? null : s;

    // Joins a group's segment names root→leaf into a device-relative path, following ParentTagGroupId.
    private static string BuildGroupPath(
        string? groupId,
        IReadOnlyDictionary<string, (string Name, string? ParentTagGroupId)> byId)
    {
        var segments = new Stack<string>();
        var guard = 0;
        while (groupId is not null && byId.TryGetValue(groupId, out var g))
        {
            segments.Push(g.Name);
            groupId = g.ParentTagGroupId;
            if (++guard > 1024)
            {
                break; // defensive: never loop forever on a malformed parent cycle
            }
        }

        return string.Join(RawPaths.SeparatorString, segments);
    }
}
