using System.Collections.Immutable;
using System.Security.Claims;
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Logging;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Utils.Collections;
using ILogger = Serilog.ILogger;

namespace Phantom.Controller.Services.Users.Permissions;

public sealed class PermissionManager {
	private static readonly ILogger Logger = PhantomLogger.Create<PermissionManager>();
	
	private readonly IDatabaseProvider databaseProvider;
	private readonly Dictionary<Guid, IdentityPermissions> userIdsToPermissionIds = new ();

	public PermissionManager(IDatabaseProvider databaseProvider) {
		this.databaseProvider = databaseProvider;
	}

	internal async Task Initialize() {
		Logger.Information("Adding default permissions to database.");
		
		await using var ctx = databaseProvider.Provide();
		
		var existingPermissionIds = await ctx.Permissions.Select(static p => p.Id).AsAsyncEnumerable().ToImmutableSetAsync();
		var missingPermissionIds = GetMissingPermissionsOrdered(Permission.All, existingPermissionIds);
		if (!missingPermissionIds.IsEmpty) {
			Logger.Information("Adding default permissions: {Permissions}", string.Join(", ", missingPermissionIds));
			
			foreach (var permissionId in missingPermissionIds) {
				ctx.Permissions.Add(new PermissionEntity(permissionId));
			}
			
			await ctx.SaveChangesAsync();
		}
	}
	
	internal static ImmutableArray<string> GetMissingPermissionsOrdered(IEnumerable<Permission> allPermissions, ImmutableHashSet<string> existingPermissionIds) {
		return allPermissions.Select(static permission => permission.Id).Except(existingPermissionIds).Order().ToImmutableArray();
	}

	private IdentityPermissions FetchPermissionsForUserId(Guid userId) {
		using var ctx = databaseProvider.Provide();
		var userPermissions = ctx.UserPermissions.Where(up => up.UserGuid == userId).Select(static up => up.PermissionId);
		var rolePermissions = ctx.UserRoles.Where(ur => ur.UserGuid == userId).Join(ctx.RolePermissions, static ur => ur.RoleGuid, static rp => rp.RoleGuid, static (ur, rp) => rp.PermissionId);
		return new IdentityPermissions(userPermissions.Union(rolePermissions));
	}

	private IdentityPermissions GetPermissionsForUserId(Guid userId, bool refreshCache) {
		if (!refreshCache && userIdsToPermissionIds.TryGetValue(userId, out var userPermissions)) {
			return userPermissions;
		}
		else {
			return userIdsToPermissionIds[userId] = FetchPermissionsForUserId(userId);
		}
	}

	public IdentityPermissions GetPermissions(ClaimsPrincipal user, bool refreshCache = false) {
		Guid? userId = UserManager.GetAuthenticatedUserId(user);
		return userId == null ? IdentityPermissions.None : GetPermissionsForUserId(userId.Value, refreshCache);
	}

	public bool CheckPermission(ClaimsPrincipal user, Permission permission, bool refreshCache = false) {
		return GetPermissions(user, refreshCache).Check(permission);
	}
}