namespace ZB.MOM.WW.OtOpcUa.Commons.Types;

/// <summary>
///     Resolves a raw <c>Tag</c>'s v3 <c>RawPath</c> — the single identity string
///     <c>&lt;Folder/…&gt;/&lt;Driver&gt;/&lt;Device&gt;/&lt;TagGroup/…&gt;/&lt;Tag&gt;</c> — from the raw
///     topology's ancestry maps, and a driver's RawPath prefix (folders + driver name). Pure + input-shape
///     agnostic: callers build the four id→ancestry maps from whatever they have (EF entities on the
///     authoring/validation side, artifact JSON on the deploy-decode side) and both sides then agree
///     byte-for-byte because the segment order + join live ONLY here (over <see cref="RawPaths"/>).
///     <para>Every path is built through <see cref="RawPaths.Combine"/>, so segment charset (no
///     <c>/</c>, no lead/trail whitespace) is enforced in one place. A broken link (missing device / driver
///     lookup, or an invalid segment) yields <see langword="null"/> — never a throw — so a malformed
///     artifact degrades to "this tag has no RawPath" (dropped) rather than faulting a whole deploy decode.
///     Ancestry walks are depth-bounded to survive a corrupt parent cycle.</para>
/// </summary>
public sealed class RawPathResolver
{
    private const int MaxAncestryDepth = 256;

    private readonly IReadOnlyDictionary<string, (string? ParentId, string Name)> _folders;
    private readonly IReadOnlyDictionary<string, (string? RawFolderId, string Name)> _drivers;
    private readonly IReadOnlyDictionary<string, (string DriverInstanceId, string Name)> _devices;
    private readonly IReadOnlyDictionary<string, (string? ParentId, string Name)> _groups;

    /// <summary>Initializes a new <see cref="RawPathResolver"/> over the raw topology's ancestry maps.</summary>
    /// <param name="folders"><c>RawFolderId → (ParentRawFolderId?, Name)</c>. <see langword="null"/> parent = cluster root.</param>
    /// <param name="drivers"><c>DriverInstanceId → (RawFolderId?, Name)</c>. <see langword="null"/> folder = cluster root.</param>
    /// <param name="devices"><c>DeviceId → (DriverInstanceId, Name)</c>.</param>
    /// <param name="groups"><c>TagGroupId → (ParentTagGroupId?, Name)</c>. <see langword="null"/> parent = directly under the device.</param>
    public RawPathResolver(
        IReadOnlyDictionary<string, (string? ParentId, string Name)> folders,
        IReadOnlyDictionary<string, (string? RawFolderId, string Name)> drivers,
        IReadOnlyDictionary<string, (string DriverInstanceId, string Name)> devices,
        IReadOnlyDictionary<string, (string? ParentId, string Name)> groups)
    {
        ArgumentNullException.ThrowIfNull(folders);
        ArgumentNullException.ThrowIfNull(drivers);
        ArgumentNullException.ThrowIfNull(devices);
        ArgumentNullException.ThrowIfNull(groups);
        _folders = folders;
        _drivers = drivers;
        _devices = devices;
        _groups = groups;
    }

    /// <summary>
    ///     Build the RawPath prefix a driver contributes to every child tag: its folder ancestry
    ///     (root → leaf) followed by the driver name. Returns <see langword="null"/> when the driver id is
    ///     unknown or any segment/link is invalid.
    /// </summary>
    /// <param name="driverInstanceId">The driver instance id.</param>
    /// <returns>The driver RawPath prefix, or <see langword="null"/> on a broken/unknown chain.</returns>
    public string? TryBuildDriverPrefix(string driverInstanceId)
    {
        if (driverInstanceId is null || !_drivers.TryGetValue(driverInstanceId, out var driver)) return null;
        try
        {
            var path = BuildFolderChain(driver.RawFolderId);
            return RawPaths.Combine(path, driver.Name);
        }
        catch (ArgumentException)
        {
            return null;
        }
    }

    /// <summary>
    ///     Build a raw tag's full RawPath:
    ///     <c>&lt;driver-prefix&gt;/&lt;Device&gt;/&lt;TagGroup ancestry root→leaf&gt;/&lt;Tag&gt;</c>.
    ///     Returns <see langword="null"/> when the device / driver lookup is missing or any segment/link is
    ///     invalid.
    /// </summary>
    /// <param name="deviceId">The tag's owning device id.</param>
    /// <param name="tagGroupId">The tag's owning tag-group id, or <see langword="null"/> when directly under the device.</param>
    /// <param name="tagName">The tag's leaf name.</param>
    /// <returns>The tag's RawPath, or <see langword="null"/> on a broken/unknown chain.</returns>
    public string? TryBuildTagPath(string deviceId, string? tagGroupId, string tagName)
    {
        if (deviceId is null || !_devices.TryGetValue(deviceId, out var device)) return null;
        var driverPrefix = TryBuildDriverPrefix(device.DriverInstanceId);
        if (driverPrefix is null) return null;
        try
        {
            var path = RawPaths.Combine(driverPrefix, device.Name);
            path = AppendGroupChain(path, tagGroupId);
            return RawPaths.Combine(path, tagName);
        }
        catch (ArgumentException)
        {
            return null;
        }
    }

    /// <summary>Build a folder ancestry path (root → leaf) for the given leaf folder id (null = cluster root ⇒ blank).</summary>
    private string? BuildFolderChain(string? leafFolderId)
    {
        if (leafFolderId is null) return null;
        var names = new List<string>();
        var current = leafFolderId;
        var depth = 0;
        while (current is not null && depth++ < MaxAncestryDepth)
        {
            if (!_folders.TryGetValue(current, out var folder))
                throw new ArgumentException($"Unknown RawFolder '{current}'.");
            names.Add(folder.Name);
            current = folder.ParentId;
        }
        names.Reverse();
        return names.Count == 0 ? null : RawPaths.Build(names);
    }

    /// <summary>Append a tag-group ancestry path (root → leaf) onto <paramref name="parentPath"/> (null group ⇒ unchanged).</summary>
    private string AppendGroupChain(string parentPath, string? leafGroupId)
    {
        if (leafGroupId is null) return parentPath;
        var names = new List<string>();
        var current = leafGroupId;
        var depth = 0;
        while (current is not null && depth++ < MaxAncestryDepth)
        {
            if (!_groups.TryGetValue(current, out var group))
                throw new ArgumentException($"Unknown TagGroup '{current}'.");
            names.Add(group.Name);
            current = group.ParentId;
        }
        names.Reverse();
        var path = parentPath;
        foreach (var name in names)
            path = RawPaths.Combine(path, name);
        return path;
    }
}
