using Microsoft.SharePoint.Client; using Microsoft.SharePoint.Client.Search.Query; using Serilog; using SharepointToolbox.Web.Core.Helpers; using SharepointToolbox.Web.Core.Models; namespace SharepointToolbox.Web.Services; public class SystemGroupTargetResolver : ISystemGroupTargetResolver { private readonly Dictionary _cache = new(StringComparer.OrdinalIgnoreCase); public async Task ResolveAsync( ClientContext ctx, SystemGroupClassification classification, CancellationToken ct) { ct.ThrowIfCancellationRequested(); var key = BuildCacheKey(ctx.Url, classification); if (key is not null && _cache.TryGetValue(key, out var cached)) return cached; SystemGroupTarget? result = null; try { result = classification.Kind switch { SystemGroupKind.LimitedAccessWeb => await ResolveWebAsync(ctx, classification.WebId!.Value, ct), SystemGroupKind.LimitedAccessList => await ResolveListAsync(ctx, classification.ListId!.Value, ct), SystemGroupKind.SharingLink => await ResolveItemAsync(ctx, classification.ItemUniqueId!.Value, classification.LinkType, ct), _ => null }; } catch (OperationCanceledException) { throw; } catch (Exception ex) { Log.Debug("System group resolve failed for {Kind}: {Error}", classification.Kind, ex.Message); } if (key is not null) _cache[key] = result; return result; } private static string? BuildCacheKey(string siteUrl, SystemGroupClassification c) => c.Kind switch { SystemGroupKind.LimitedAccessWeb => $"{siteUrl}|web|{c.WebId}", SystemGroupKind.LimitedAccessList => $"{siteUrl}|list|{c.ListId}", SystemGroupKind.SharingLink => $"{siteUrl}|item|{c.ItemUniqueId}", _ => null }; private static async Task ResolveWebAsync(ClientContext ctx, Guid webId, CancellationToken ct) { var web = ctx.Site.OpenWebById(webId); ctx.Load(web, w => w.Title, w => w.Url); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct); return new SystemGroupTarget(SystemGroupKind.LimitedAccessWeb, web.Title, web.Url); } private static async Task ResolveListAsync(ClientContext ctx, Guid listId, CancellationToken ct) { var list = ctx.Web.Lists.GetById(listId); ctx.Load(list, l => l.Title, l => l.DefaultViewUrl); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct); return new SystemGroupTarget(SystemGroupKind.LimitedAccessList, list.Title, BuildAbsoluteUrl(ctx.Url, list.DefaultViewUrl)); } private static async Task ResolveItemAsync(ClientContext ctx, Guid itemUniqueId, string? linkType, CancellationToken ct) { try { var file = ctx.Web.GetFileById(itemUniqueId); ctx.Load(file, f => f.Name, f => f.ServerRelativeUrl, f => f.Exists); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct); if (file.Exists) return new SystemGroupTarget(SystemGroupKind.SharingLink, file.Name, BuildAbsoluteUrl(ctx.Url, file.ServerRelativeUrl), linkType); } catch (ServerException ex) { Log.Debug("File by ID not found: {Error}", ex.Message); } try { var folder = ctx.Web.GetFolderById(itemUniqueId); ctx.Load(folder, f => f.Name, f => f.ServerRelativeUrl); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct); return new SystemGroupTarget(SystemGroupKind.SharingLink, folder.Name, BuildAbsoluteUrl(ctx.Url, folder.ServerRelativeUrl), linkType); } catch (ServerException ex) { Log.Debug("Folder by ID not found: {Error}", ex.Message); } return await TryResolveViaSearchAsync(ctx, itemUniqueId, linkType, ct); } private static async Task TryResolveViaSearchAsync(ClientContext ctx, Guid itemUniqueId, string? linkType, CancellationToken ct) { ct.ThrowIfCancellationRequested(); try { var kq = new KeywordQuery(ctx) { QueryText = $"UniqueId:{{{itemUniqueId}}}", RowLimit = 1, TrimDuplicates = false }; kq.SelectProperties.Add("Path"); kq.SelectProperties.Add("Title"); var executor = new SearchExecutor(ctx); var result = executor.ExecuteQuery(kq); await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct); var table = result.Value.FirstOrDefault(t => t.TableType == KnownTableTypes.RelevantResults); if (table is null || table.RowCount == 0) return null; var row = ToDict(table.ResultRows.First()); var path = row.TryGetValue("Path", out var p) ? p?.ToString() : null; var title = row.TryGetValue("Title", out var t) ? t?.ToString() : null; if (string.IsNullOrEmpty(path)) return null; var leaf = !string.IsNullOrWhiteSpace(title) ? title! : Uri.UnescapeDataString(path.TrimEnd('/').Split('/').Last()); return new SystemGroupTarget(SystemGroupKind.SharingLink, $"{leaf} (via index)", path, linkType); } catch (OperationCanceledException) { throw; } catch (Exception ex) { Log.Debug("UniqueId search fallback failed: {Error}", ex.Message); return null; } } private static IDictionary ToDict(object rawRow) { if (rawRow is IDictionary generic) return generic; var dict = new Dictionary(); if (rawRow is System.Collections.IDictionary legacy) foreach (System.Collections.DictionaryEntry e in legacy) dict[e.Key.ToString()!] = e.Value ?? string.Empty; return dict; } private static string BuildAbsoluteUrl(string contextUrl, string? serverRelative) { if (string.IsNullOrEmpty(serverRelative)) return contextUrl; if (Uri.TryCreate(serverRelative, UriKind.Absolute, out _)) return serverRelative; var uri = new Uri(contextUrl); return $"{uri.Scheme}://{uri.Host}{serverRelative}"; } }