using ZB.MOM.WW.GalaxyRepository.Grpc;

namespace ZB.MOM.WW.GalaxyRepository;

/// <summary>
///     Precomputed lookup structures over a materialized Galaxy object list. Built once per
///     cache entry so browse/discover handlers can resolve roots/parents by id, tag name, or
///     contained path in O(1), enumerate direct children, and resolve tag addresses to objects
///     or attributes without rescanning the full object list.
/// </summary>
public sealed class GalaxyHierarchyIndex
{
    private GalaxyHierarchyIndex(
        IReadOnlyList<GalaxyObjectView> objectViews,
        IReadOnlyDictionary<int, GalaxyObjectView> objectViewsById,
        IReadOnlyDictionary<string, GalaxyTagLookup> tagsByAddress,
        IReadOnlyDictionary<int, IReadOnlyList<GalaxyObjectView>> childrenByParent,
        IReadOnlyDictionary<string, GalaxyObjectView> objectViewsByTagName,
        IReadOnlyDictionary<string, GalaxyObjectView> objectViewsByContainedPath)
    {
        ObjectViews = objectViews;
        ObjectViewsById = objectViewsById;
        TagsByAddress = tagsByAddress;
        ChildrenByParent = childrenByParent;
        ObjectViewsByTagName = objectViewsByTagName;
        ObjectViewsByContainedPath = objectViewsByContainedPath;
    }

    /// <summary>Gets an empty Galaxy hierarchy index.</summary>
    public static GalaxyHierarchyIndex Empty { get; } = new(
        Array.Empty<GalaxyObjectView>(),
        new Dictionary<int, GalaxyObjectView>(),
        new Dictionary<string, GalaxyTagLookup>(StringComparer.OrdinalIgnoreCase),
        new Dictionary<int, IReadOnlyList<GalaxyObjectView>>(),
        new Dictionary<string, GalaxyObjectView>(StringComparer.OrdinalIgnoreCase),
        new Dictionary<string, GalaxyObjectView>(StringComparer.OrdinalIgnoreCase));

    /// <summary>Gets the object views.</summary>
    public IReadOnlyList<GalaxyObjectView> ObjectViews { get; }

    /// <summary>Gets the object views indexed by gobject id.</summary>
    public IReadOnlyDictionary<int, GalaxyObjectView> ObjectViewsById { get; }

    /// <summary>Gets tags indexed by address.</summary>
    public IReadOnlyDictionary<string, GalaxyTagLookup> TagsByAddress { get; }

    /// <summary>Gets direct children grouped by parent gobject id. Root objects (no parent, or self-parented) live under key 0. Each list is sorted areas-first, then by display name (OrdinalIgnoreCase).</summary>
    public IReadOnlyDictionary<int, IReadOnlyList<GalaxyObjectView>> ChildrenByParent { get; }

    /// <summary>Gets object views indexed by <see cref="GalaxyObject.TagName"/> (OrdinalIgnoreCase). Lets browse/discover handlers resolve parents/roots by tag name in O(1) instead of scanning <see cref="ObjectViews"/>.</summary>
    public IReadOnlyDictionary<string, GalaxyObjectView> ObjectViewsByTagName { get; }

    /// <summary>Gets object views indexed by contained path (OrdinalIgnoreCase). Lets browse/discover handlers resolve parents/roots by path in O(1) instead of scanning <see cref="ObjectViews"/>.</summary>
    public IReadOnlyDictionary<string, GalaxyObjectView> ObjectViewsByContainedPath { get; }

    /// <summary>Builds a Galaxy hierarchy index from the given objects.</summary>
    /// <param name="objects">The Galaxy objects to index.</param>
    /// <returns>A new Galaxy hierarchy index.</returns>
    public static GalaxyHierarchyIndex Build(IReadOnlyList<GalaxyObject> objects)
    {
        if (objects.Count == 0)
        {
            return Empty;
        }

        Dictionary<int, GalaxyObject> objectsById = new();
        foreach (GalaxyObject obj in objects)
        {
            objectsById.TryAdd(obj.GobjectId, obj);
        }

        List<GalaxyObjectView> views = new(objects.Count);
        Dictionary<int, GalaxyObjectView> viewsById = new();
        Dictionary<string, GalaxyTagLookup> tagsByAddress = new(StringComparer.OrdinalIgnoreCase);
        Dictionary<string, GalaxyObjectView> viewsByTagName = new(StringComparer.OrdinalIgnoreCase);
        Dictionary<string, GalaxyObjectView> viewsByContainedPath = new(StringComparer.OrdinalIgnoreCase);

        foreach (GalaxyObject obj in objects)
        {
            string path = BuildContainedPath(obj, objectsById);
            int depth = string.IsNullOrWhiteSpace(path) ? 0 : path.Count(character => character == '/');
            GalaxyObjectView view = new(obj, path, depth);
            views.Add(view);
            viewsById.TryAdd(obj.GobjectId, view);

            if (!string.IsNullOrWhiteSpace(obj.TagName))
            {
                tagsByAddress.TryAdd(obj.TagName, new GalaxyTagLookup(obj, Attribute: null, path));
                viewsByTagName.TryAdd(obj.TagName, view);
            }

            if (!string.IsNullOrWhiteSpace(path))
            {
                viewsByContainedPath.TryAdd(path, view);
            }

            foreach (GalaxyAttribute attribute in obj.Attributes)
            {
                if (!string.IsNullOrWhiteSpace(attribute.FullTagReference))
                {
                    tagsByAddress.TryAdd(attribute.FullTagReference, new GalaxyTagLookup(obj, attribute, path));
                }
            }
        }

        Dictionary<int, List<GalaxyObjectView>> childrenByParent = new();
        foreach (GalaxyObjectView view in views)
        {
            int parentKey = view.Object.ParentGobjectId;
            // Treat self-parented (corrupt) rows as roots.
            if (parentKey == view.Object.GobjectId)
            {
                parentKey = 0;
            }
            // Re-root orphans whose parent object is absent from the set (e.g. a deleted or
            // never-loaded container area). Otherwise they bucket under a phantom parent id
            // that is never reached from the root, so they vanish from browse entirely.
            else if (parentKey != 0 && !objectsById.ContainsKey(parentKey))
            {
                parentKey = 0;
            }
            if (!childrenByParent.TryGetValue(parentKey, out List<GalaxyObjectView>? bucket))
            {
                bucket = [];
                childrenByParent[parentKey] = bucket;
            }
            bucket.Add(view);
        }

        foreach (List<GalaxyObjectView> bucket in childrenByParent.Values)
        {
            bucket.Sort(CompareByAreaThenDisplayName);
        }

        Dictionary<int, IReadOnlyList<GalaxyObjectView>> readOnlyChildren = new(childrenByParent.Count);
        foreach (KeyValuePair<int, List<GalaxyObjectView>> kvp in childrenByParent)
        {
            readOnlyChildren[kvp.Key] = kvp.Value;
        }

        return new GalaxyHierarchyIndex(
            views,
            viewsById,
            tagsByAddress,
            readOnlyChildren,
            viewsByTagName,
            viewsByContainedPath);
    }

    private static string BuildContainedPath(
        GalaxyObject obj,
        IReadOnlyDictionary<int, GalaxyObject> objectsById)
    {
        Stack<string> names = new();
        HashSet<int> seen = [];
        GalaxyObject? current = obj;
        while (current is not null && seen.Add(current.GobjectId))
        {
            names.Push(ResolvePathSegment(current));
            current = current.ParentGobjectId != 0
                && objectsById.TryGetValue(current.ParentGobjectId, out GalaxyObject? parent)
                    ? parent
                    : null;
        }

        return string.Join('/', names.Where(name => !string.IsNullOrWhiteSpace(name)));
    }

    private static string ResolvePathSegment(GalaxyObject obj)
    {
        if (!string.IsNullOrWhiteSpace(obj.ContainedName))
        {
            return obj.ContainedName;
        }

        if (!string.IsNullOrWhiteSpace(obj.BrowseName))
        {
            return obj.BrowseName;
        }

        return obj.TagName;
    }

    private static int CompareByAreaThenDisplayName(GalaxyObjectView left, GalaxyObjectView right)
    {
        if (left.Object.IsArea != right.Object.IsArea)
        {
            return left.Object.IsArea ? -1 : 1;
        }
        return string.Compare(DisplayNameOf(left), DisplayNameOf(right), StringComparison.OrdinalIgnoreCase);
    }

    private static string DisplayNameOf(GalaxyObjectView view)
    {
        GalaxyObject obj = view.Object;
        if (!string.IsNullOrWhiteSpace(obj.BrowseName))
        {
            return obj.BrowseName;
        }
        if (!string.IsNullOrWhiteSpace(obj.ContainedName))
        {
            return obj.ContainedName;
        }
        return obj.TagName;
    }
}
