using Microsoft.SharePoint.Client;
using Microsoft.SharePoint.Client.Search.Query;
using Serilog;
using SharepointToolbox.Core.Helpers;
using SharepointToolbox.Core.Models;

namespace SharepointToolbox.Services;

/// <summary>
/// CSOM-backed resolver that converts the GUID/identifier embedded in a SharePoint
/// system group name into a clickable target. Results are cached per
/// (site URL, guid) to avoid repeated round-trips when the same Limited Access
/// system group recurs on many objects.
///
/// Never throws — on any failure (not found, access denied, claims error), returns
/// <c>null</c> and the caller renders the raw group name.
/// </summary>
public class SystemGroupTargetResolver : ISystemGroupTargetResolver
{
    private readonly Dictionary<string, SystemGroupTarget?> _cache =
        new(StringComparer.OrdinalIgnoreCase);

    public async Task<SystemGroupTarget?> 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 target resolution failed for {Kind} on {Site}: {Error}",
                classification.Kind, ctx.Url, 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<SystemGroupTarget?> 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<SystemGroupTarget?> 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);

        var url = BuildAbsoluteUrl(ctx.Url, list.DefaultViewUrl);
        return new SystemGroupTarget(SystemGroupKind.LimitedAccessList, list.Title, url);
    }

    private static async Task<SystemGroupTarget?> ResolveItemAsync(
        ClientContext ctx, Guid itemUniqueId, string? linkType, CancellationToken ct)
    {
        // 1. Try as file on current web (most sharing links target files).
        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)
            {
                var url = BuildAbsoluteUrl(ctx.Url, file.ServerRelativeUrl);
                return new SystemGroupTarget(SystemGroupKind.SharingLink, file.Name, url, linkType);
            }
        }
        catch (ServerException ex) { Log.Debug("File by ID not found for {Id} on {Site}: {Error}", itemUniqueId, ctx.Url, ex.Message); }

        // 2. Try as folder on current web.
        try
        {
            var folder = ctx.Web.GetFolderById(itemUniqueId);
            ctx.Load(folder, f => f.Name, f => f.ServerRelativeUrl);
            await ExecuteQueryRetryHelper.ExecuteQueryRetryAsync(ctx, null, ct);

            var url = BuildAbsoluteUrl(ctx.Url, folder.ServerRelativeUrl);
            return new SystemGroupTarget(SystemGroupKind.SharingLink, folder.Name, url, linkType);
        }
        catch (ServerException ex) { Log.Debug("Folder by ID not found for {Id} on {Site}: {Error}", itemUniqueId, ctx.Url, ex.Message); }

        // 3. Search-index fallback — covers items moved to a different subsite or
        //    deleted recently (the index may lag the deletion by minutes/hours).
        //    Search is permission-trimmed, so this only returns hits the caller
        //    can still see; results carry a "(via index)" hint in the label.
        return await TryResolveViaSearchAsync(ctx, itemUniqueId, linkType, ct);
    }

    /// <summary>
    /// Queries the SharePoint search index for an item by its UniqueId. Returns
    /// a target with the last-indexed path and a label prefix flagging that the
    /// hit came from the index (so admins know the live lookup failed).
    /// </summary>
    private static async Task<SystemGroupTarget?> TryResolveViaSearchAsync(
        ClientContext ctx, Guid itemUniqueId, string? linkType, CancellationToken ct)
    {
        ct.ThrowIfCancellationRequested();
        try
        {
            // KQL: UniqueId managed property. Braces around the GUID are how the
            // SP search engine matches the canonical form for content unique ids.
            var kq = new KeywordQuery(ctx)
            {
                QueryText      = $"UniqueId:{{{itemUniqueId}}}",
                RowLimit       = 1,
                TrimDuplicates = false
            };
            kq.SelectProperties.Add("Path");
            kq.SelectProperties.Add("Title");
            kq.SelectProperties.Add("FileExtension");

            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;

            // Derive a leaf name if Title is empty (folders often have no Title).
            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 for {Item} on {Site}: {Error}",
                itemUniqueId, ctx.Url, ex.Message);
            return null;
        }
    }

    /// <summary>
    /// CSOM SearchExecutor returns ResultRows as either generic Dictionary or legacy
    /// IDictionary depending on CSOM version — normalise to a single shape.
    /// </summary>
    private static IDictionary<string, object> ToDict(object rawRow)
    {
        if (rawRow is IDictionary<string, object> generic)
            return generic;
        var dict = new Dictionary<string, object>();
        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}";
    }
}
