using Raven.Client.Documents;
using Raven.Client.Documents.Linq;
using System.Collections.Concurrent;
using System.Linq.Expressions;

namespace zero.Routing;

public class PageRouteResolverHelper
{
  protected ConcurrentDictionary<string, HashSet<(Page, Route)>> PageRouteCache { get; set; } = new();


  public async Task<Route> ResolveFor<T>(RoutingContext context, Func<Page, bool> predicate = null)
  {
    return (await ResolveAllFor<T>(context, predicate)).FirstOrDefault();
  }


  public async Task<Page> ResolvePageFor<T>(RoutingContext context, Func<Page, bool> predicate = null)
  {
    return (await ResolveAllPagesFor<T>(context, predicate)).FirstOrDefault();
  }


  public async Task<HashSet<Route>> ResolveAllFor<T>(RoutingContext context, Func<Page, bool> predicate = null)
  {
    RoutingOptions options = context.Context.Options.For<RoutingOptions>();

    string fullKey = typeof(T).Name.ToString().ToLower() + ":" + context.Context.AppId;

    if (!PageRouteCache.TryGetValue(fullKey, out HashSet<(Page, Route)> routes))
    {
      List<Page> pages = new();
      IEnumerable<Expression<Func<Page, bool>>> resolvers = options.PageResolvers.GetAll(typeof(T));

      foreach (Expression<Func<Page, bool>> resolver in resolvers.Reverse())
      {
        pages.AddRange(await context.Session.Query<Page>().Where(resolver).ToListAsync());
      }

      Dictionary<string, Page> idToPage = pages.ToDictionary(x => options.PageRouteIdBuilder.Generate(x), x => x);
      Dictionary<string, Route> idToRoute = await context.Session.LoadAsync<Route>(idToPage.Select(x => x.Key));

      routes = new();

      foreach ((string id, Page page) in idToPage)
      {
        if (idToRoute.TryGetValue(id, out Route route) && route != null)
        {
          routes.Add((page, route));
        }
      }

      PageRouteCache.TryAdd(fullKey, routes);
    }

    if (predicate != null)
    {
      return routes.Where(x => predicate(x.Item1)).Select(x => x.Item2).ToHashSet();
    }

    return routes.Select(x => x.Item2).ToHashSet();
  }


  public async Task<HashSet<Page>> ResolveAllPagesFor<T>(RoutingContext context, Func<Page, bool> predicate = null)
  {
    RoutingOptions options = context.Context.Options.For<RoutingOptions>();
    string fullKey = typeof(T).Name.ToString().ToLower() + ":" + context.Context.AppId;

    if (!PageRouteCache.TryGetValue(fullKey, out HashSet<(Page, Route)> routes))
    {
      List<Page> pages = new();
      IEnumerable<Expression<Func<Page, bool>>> resolvers = options.PageResolvers.GetAll(typeof(T));

      foreach (Expression<Func<Page, bool>> resolver in resolvers.Reverse())
      {
        pages.AddRange(await context.Session.Query<Page>().Where(resolver).ToListAsync());
      }

      Dictionary<string, Page> idToPage = pages.ToDictionary(x => options.PageRouteIdBuilder.Generate(x), x => x);
      Dictionary<string, Route> idToRoute = await context.Session.LoadAsync<Route>(idToPage.Select(x => x.Key));

      routes = new();

      foreach ((string id, Page page) in idToPage)
      {
        if (idToRoute.TryGetValue(id, out Route route) && route != null)
        {
          routes.Add((page, route));
        }
      }

      PageRouteCache.TryAdd(fullKey, routes);
    }

    if (predicate != null)
    {
      return routes.Where(x => predicate(x.Item1)).Select(x => x.Item1).ToHashSet();
    }

    return routes.Select(x => x.Item1).ToHashSet();
  }


  public async Task<Dictionary<Page, Route>> ResolveAllForAsDictionary<T>(RoutingContext context, Func<(Page, Route), bool> predicate = null)
  {
    RoutingOptions options = context.Context.Options.For<RoutingOptions>();
    string fullKey = typeof(T).Name.ToString().ToLower() + ":" + context.Context.AppId;

    if (!PageRouteCache.TryGetValue(fullKey, out HashSet<(Page, Route)> routes))
    {
      List<Page> pages = new();
      IEnumerable<Expression<Func<Page, bool>>> resolvers = options.PageResolvers.GetAll(typeof(T));

      foreach (Expression<Func<Page, bool>> resolver in resolvers.Reverse())
      {
        pages.AddRange(await context.Session.Query<Page>().Where(resolver).ToListAsync());
      }

      Dictionary<string, Page> idToPage = pages.ToDictionary(x => options.PageRouteIdBuilder.Generate(x), x => x);
      Dictionary<string, Route> idToRoute = await context.Session.LoadAsync<Route>(idToPage.Select(x => x.Key));

      routes = new();

      foreach ((string id, Page page) in idToPage)
      {
        if (idToRoute.TryGetValue(id, out Route route) && route != null)
        {
          routes.Add((page, route));
        }
      }

      PageRouteCache.TryAdd(fullKey, routes);
    }

    if (predicate != null)
    {
      return routes.Where(x => predicate(x)).ToDictionary(x => x.Item1, x => x.Item2);
    }

    return routes.ToDictionary(x => x.Item1, x => x.Item2);
  }


  public async Task<bool> IsRelevantFor<T>(RoutingContext context, Page page)
  {
    HashSet<Page> pages = await ResolveAllPagesFor<T>(context);

    if (pages.Any(x => x.Id == page.Id))
    {
      return true;
    }

    string[] childIds = await GetChildIds(context, page.Id);
    return pages.Any(x => childIds.Contains(x.Id));
  }


  /// <summary>
  /// Get relevant pages for a page
  /// </summary>
  public virtual async Task<List<Page>> GetRelevantPagesFor<T>(RoutingContext context, Page page)
  {
    HashSet<Page> pages = await ResolveAllPagesFor<T>(context);
    string[] childIds = await GetChildIds(context, page.Id);
    return pages.Where(x => childIds.Contains(x.Id) || x.Id == page.Id).ToList();
  }


  /// <summary>
  /// Get parents for a page
  /// </summary>
  protected virtual async Task<string[]> GetChildIds(RoutingContext context, params string[] pageIds)
  {
    return await context.Session.Query<zero_Pages_ByHierarchy.Result, zero_Pages_ByHierarchy>()
      .Where(x => x.PathIds.In(pageIds))
      .Select(x => x.Id)
      .ToArrayAsync();
  }
}
