using System.Collections.Concurrent;
using System.Runtime.CompilerServices;
using System.Security.Cryptography;
using System.Text;
using Grpc.Core;
using ZB.MOM.WW.GalaxyRepository.Grpc;

namespace ZB.MOM.WW.GalaxyRepository;

/// <summary>
///     Projects a <c>DiscoverHierarchy</c> request against an immutable
///     <see cref="GalaxyHierarchyCacheEntry"/>: applies the root/depth/category/template/glob
///     filters, pages the result, and memoizes the filtered list per cache-entry instance so
///     paging is O(pageSize) rather than O(total) per page. Pure and side-effect free.
/// </summary>
public static class GalaxyHierarchyProjector
{
    /// <summary>
    ///     Per-cache-entry memo of filtered, ordered <see cref="GalaxyObjectView"/> lists
    ///     keyed by filter signature. Without it, paging through a large hierarchy
    ///     re-applies every filter and re-scans the full <see cref="GalaxyHierarchyIndex.ObjectViews"/>
    ///     collection on every page — O(total) per page, O(total²/pageSize) end-to-end.
    ///     With it, the first page builds the filtered list and each subsequent page is an
    ///     O(pageSize) slice. The table is keyed on the immutable cache-entry instance, so
    ///     when the cache publishes a new entry the stale memo becomes unreachable and is
    ///     reclaimed with it — no explicit invalidation needed.
    /// </summary>
    private static readonly ConditionalWeakTable<GalaxyHierarchyCacheEntry, ConcurrentDictionary<string, IReadOnlyList<GalaxyObjectView>>> FilteredViewCache = new();

    /// <summary>Projects a discovery request against a cache entry and returns all matching objects.</summary>
    /// <param name="entry">The Galaxy hierarchy cache entry.</param>
    /// <param name="request">The discovery hierarchy request.</param>
    /// <param name="browseSubtreeGlobs">Optional glob patterns to filter browse subtrees.</param>
    public static GalaxyHierarchyQueryResult Project(
        GalaxyHierarchyCacheEntry entry,
        DiscoverHierarchyRequest request,
        IReadOnlyList<string>? browseSubtreeGlobs = null)
    {
        return Project(
            entry,
            request,
            browseSubtreeGlobs,
            offset: 0,
            pageSize: int.MaxValue);
    }

    /// <summary>Projects a discovery request with paging against a cache entry and returns a page of matching objects.</summary>
    /// <param name="entry">The Galaxy hierarchy cache entry.</param>
    /// <param name="request">The discovery hierarchy request.</param>
    /// <param name="browseSubtreeGlobs">Optional glob patterns to filter browse subtrees.</param>
    /// <param name="offset">The zero-based offset into the result set.</param>
    /// <param name="pageSize">The maximum number of results to return.</param>
    public static GalaxyHierarchyQueryResult Project(
        GalaxyHierarchyCacheEntry entry,
        DiscoverHierarchyRequest request,
        IReadOnlyList<string>? browseSubtreeGlobs,
        int offset,
        int pageSize)
    {
        ArgumentNullException.ThrowIfNull(entry);
        ArgumentNullException.ThrowIfNull(request);
        if (offset < 0)
        {
            throw new ArgumentOutOfRangeException(nameof(offset), offset, "Offset must be greater than or equal to zero.");
        }

        if (pageSize <= 0)
        {
            throw new ArgumentOutOfRangeException(nameof(pageSize), pageSize, "Page size must be greater than zero.");
        }

        int? maxDepth = request.MaxDepth;
        if (maxDepth < 0)
        {
            throw new RpcException(new Status(
                StatusCode.InvalidArgument,
                "DiscoverHierarchy max_depth must be greater than or equal to zero when provided."));
        }

        string filterSignature = ComputeFilterSignature(request, browseSubtreeGlobs);
        IReadOnlyList<GalaxyObjectView> matchedViews = GetFilteredViews(
            entry,
            request,
            browseSubtreeGlobs,
            maxDepth,
            filterSignature);

        bool includeAttributes = IncludeAttributes(request);
        List<GalaxyObject> page = new(Math.Min(pageSize, Math.Max(0, matchedViews.Count - offset)));
        int end = (int)Math.Min((long)offset + pageSize, matchedViews.Count);
        for (int index = offset; index < end; index++)
        {
            page.Add(CloneObject(matchedViews[index].Object, includeAttributes));
        }

        return new GalaxyHierarchyQueryResult(
            page,
            matchedViews.Count,
            filterSignature);
    }

    private static IReadOnlyList<GalaxyObjectView> GetFilteredViews(
        GalaxyHierarchyCacheEntry entry,
        DiscoverHierarchyRequest request,
        IReadOnlyList<string>? browseSubtreeGlobs,
        int? maxDepth,
        string filterSignature)
    {
        // ResolveRoot can throw RpcException(NotFound); run it before consulting the
        // memo so a bad root surfaces consistently regardless of cache state.
        IReadOnlyList<GalaxyObjectView> views = entry.Index.ObjectViews;
        GalaxyObjectView? root = ResolveRoot(request, entry.Index);

        ConcurrentDictionary<string, IReadOnlyList<GalaxyObjectView>> memo =
            FilteredViewCache.GetValue(entry, static _ => new ConcurrentDictionary<string, IReadOnlyList<GalaxyObjectView>>(StringComparer.Ordinal));

        return memo.GetOrAdd(
            filterSignature,
            static (_, state) =>
            {
                List<GalaxyObjectView> matched = [];
                foreach (GalaxyObjectView view in state.Views)
                {
                    if (MatchesRoot(view, state.Root, state.MaxDepth)
                        && MatchesBrowseSubtrees(view, state.BrowseSubtreeGlobs)
                        && MatchesFilters(view.Object, state.Request))
                    {
                        matched.Add(view);
                    }
                }

                return matched;
            },
            (Views: views, Root: root, MaxDepth: maxDepth, BrowseSubtreeGlobs: browseSubtreeGlobs, Request: request));
    }

    /// <summary>Finds an object in the hierarchy by its tag address.</summary>
    /// <param name="entry">The Galaxy hierarchy cache entry.</param>
    /// <param name="tagAddress">The tag address to search for.</param>
    public static GalaxyObject? FindObjectForTag(
        GalaxyHierarchyCacheEntry entry,
        string tagAddress)
    {
        if (string.IsNullOrWhiteSpace(tagAddress))
        {
            return null;
        }

        return entry.Index.TagsByAddress.TryGetValue(tagAddress, out GalaxyTagLookup? lookup)
            ? lookup.Object
            : null;
    }

    /// <summary>Finds an attribute in the hierarchy by its tag address.</summary>
    /// <param name="entry">The Galaxy hierarchy cache entry.</param>
    /// <param name="tagAddress">The tag address to search for.</param>
    public static GalaxyAttribute? FindAttributeForTag(
        GalaxyHierarchyCacheEntry entry,
        string tagAddress)
    {
        if (string.IsNullOrWhiteSpace(tagAddress))
        {
            return null;
        }

        return entry.Index.TagsByAddress.TryGetValue(tagAddress, out GalaxyTagLookup? lookup)
            ? lookup.Attribute
            : null;
    }

    /// <summary>Gets the contained path for an object by its gobject ID.</summary>
    /// <param name="entry">The Galaxy hierarchy cache entry.</param>
    /// <param name="gobjectId">The Galaxy object ID.</param>
    public static string GetContainedPath(
        GalaxyHierarchyCacheEntry entry,
        int gobjectId)
    {
        return entry.Index.ObjectViewsById.TryGetValue(gobjectId, out GalaxyObjectView? view)
            ? view.ContainedPath
            : string.Empty;
    }

    private static GalaxyObjectView? ResolveRoot(
        DiscoverHierarchyRequest request,
        GalaxyHierarchyIndex index)
    {
        GalaxyObjectView? root = request.RootCase switch
        {
            DiscoverHierarchyRequest.RootOneofCase.None => null,
            DiscoverHierarchyRequest.RootOneofCase.RootGobjectId =>
                index.ObjectViewsById.TryGetValue(request.RootGobjectId, out GalaxyObjectView? byId) ? byId : null,
            DiscoverHierarchyRequest.RootOneofCase.RootTagName =>
                index.ObjectViewsByTagName.TryGetValue(request.RootTagName, out GalaxyObjectView? byTag) ? byTag : null,
            DiscoverHierarchyRequest.RootOneofCase.RootContainedPath =>
                index.ObjectViewsByContainedPath.TryGetValue(request.RootContainedPath, out GalaxyObjectView? byPath) ? byPath : null,
            _ => null,
        };

        if (request.RootCase != DiscoverHierarchyRequest.RootOneofCase.None && root is null)
        {
            throw new RpcException(new Status(StatusCode.NotFound, "DiscoverHierarchy root was not found."));
        }

        return root;
    }

    private static bool MatchesRoot(
        GalaxyObjectView view,
        GalaxyObjectView? root,
        int? maxDepth)
    {
        if (root is null)
        {
            return true;
        }

        bool isRoot = view.Object.GobjectId == root.Object.GobjectId;
        bool isDescendant = view.ContainedPath.StartsWith(root.ContainedPath + "/", StringComparison.OrdinalIgnoreCase);
        if (!isRoot && !isDescendant)
        {
            return false;
        }

        return maxDepth is null || view.Depth - root.Depth <= maxDepth.Value;
    }

    private static bool MatchesBrowseSubtrees(
        GalaxyObjectView view,
        IReadOnlyList<string>? browseSubtreeGlobs)
    {
        return browseSubtreeGlobs is null
            || browseSubtreeGlobs.Count == 0
            || browseSubtreeGlobs.Any(glob => GalaxyGlobMatcher.IsMatch(view.ContainedPath, glob));
    }

    private static bool MatchesFilters(
        GalaxyObject obj,
        DiscoverHierarchyRequest request)
    {
        if (request.CategoryIds.Count > 0 && !request.CategoryIds.Contains(obj.CategoryId))
        {
            return false;
        }

        foreach (string templateFilter in request.TemplateChainContains)
        {
            if (!obj.TemplateChain.Any(template => template.Contains(templateFilter, StringComparison.OrdinalIgnoreCase)))
            {
                return false;
            }
        }

        if (!string.IsNullOrWhiteSpace(request.TagNameGlob)
            && !GalaxyGlobMatcher.IsMatch(obj.TagName, request.TagNameGlob))
        {
            return false;
        }

        if (request.AlarmBearingOnly && !obj.Attributes.Any(attribute => attribute.IsAlarm))
        {
            return false;
        }

        if (request.HistorizedOnly && !obj.Attributes.Any(attribute => attribute.IsHistorized))
        {
            return false;
        }

        return true;
    }

    private static bool IncludeAttributes(DiscoverHierarchyRequest request)
    {
        return !request.HasIncludeAttributes || request.IncludeAttributes;
    }

    private static GalaxyObject CloneObject(GalaxyObject source, bool includeAttributes)
    {
        GalaxyObject clone = source.Clone();
        if (!includeAttributes)
        {
            clone.Attributes.Clear();
        }

        return clone;
    }

    /// <summary>Computes a stable filter signature for memoization purposes.</summary>
    /// <param name="request">The discovery hierarchy request.</param>
    /// <param name="browseSubtreeGlobs">Optional glob patterns to filter browse subtrees.</param>
    public static string ComputeFilterSignature(
        DiscoverHierarchyRequest request,
        IReadOnlyList<string>? browseSubtreeGlobs)
    {
        StringBuilder builder = new();
        builder.Append("root=").Append(request.RootCase).Append('|');
        builder.Append(request.RootCase switch
        {
            DiscoverHierarchyRequest.RootOneofCase.RootGobjectId => request.RootGobjectId.ToString(
                System.Globalization.CultureInfo.InvariantCulture),
            DiscoverHierarchyRequest.RootOneofCase.RootTagName => request.RootTagName,
            DiscoverHierarchyRequest.RootOneofCase.RootContainedPath => request.RootContainedPath,
            _ => string.Empty,
        });
        builder.Append("|max=").Append(request.MaxDepth?.ToString(System.Globalization.CultureInfo.InvariantCulture) ?? "");
        builder.Append("|cat=").AppendJoin(',', request.CategoryIds.Order());
        builder.Append("|tpl=").AppendJoin(',', request.TemplateChainContains.Order(StringComparer.OrdinalIgnoreCase));
        builder.Append("|glob=").Append(request.TagNameGlob);
        builder.Append("|attrs=").Append(request.HasIncludeAttributes ? request.IncludeAttributes.ToString() : "unset");
        builder.Append("|alarm=").Append(request.AlarmBearingOnly);
        builder.Append("|hist=").Append(request.HistorizedOnly);
        builder.Append("|browse=").AppendJoin(',', (browseSubtreeGlobs ?? Array.Empty<string>()).Order(StringComparer.OrdinalIgnoreCase));

        byte[] hash = SHA256.HashData(Encoding.UTF8.GetBytes(builder.ToString()));
        return Convert.ToHexString(hash, 0, 12);
    }
}
