using System.Collections.Immutable;
using Microsoft.EntityFrameworkCore;
using Phantom.Common.Logging;
using Phantom.Controller.Database;
using Phantom.Controller.Database.Entities;
using Phantom.Controller.Services.Users.Permissions;
using Phantom.Utils.Collections;
using Phantom.Utils.Tasks;
using ILogger = Serilog.ILogger;

namespace Phantom.Controller.Services.Users.Roles;

public sealed class RoleManager {
	private static readonly ILogger Logger = PhantomLogger.Create<RoleManager>();

	private const int MaxRoleNameLength = 40;

	private readonly IDatabaseProvider databaseProvider;

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

	internal async Task Initialize() {
		Logger.Information("Adding default roles to database.");
		
		await using var ctx = databaseProvider.Provide();

		var existingRoleNames = await ctx.Roles
		                                 .Select(static role => role.Name)
		                                 .AsAsyncEnumerable()
		                                 .ToImmutableSetAsync();
		
		var existingPermissionIdsByRoleGuid = await ctx.RolePermissions
		                                               .GroupBy(static rp => rp.RoleGuid, static rp => rp.PermissionId)
		                                               .ToDictionaryAsync(static g => g.Key, static g => g.ToImmutableHashSet());
		
		foreach (var role in Role.All) {
			if (!existingRoleNames.Contains(role.Name)) {
				Logger.Information("Adding default role \"{Name}\".", role.Name);
				ctx.Roles.Add(new RoleEntity(role.Guid, role.Name));
			}
			
			var existingPermissionIds = existingPermissionIdsByRoleGuid.TryGetValue(role.Guid, out var ids) ? ids : ImmutableHashSet<string>.Empty;
			var missingPermissionIds = PermissionManager.GetMissingPermissionsOrdered(role.Permissions, existingPermissionIds);
			if (!missingPermissionIds.IsEmpty) {
				Logger.Information("Assigning default permission to role \"{Name}\": {Permissions}", role.Name, string.Join(", ", missingPermissionIds));
				foreach (var permissionId in missingPermissionIds) {
					ctx.RolePermissions.Add(new RolePermissionEntity(role.Guid, permissionId));
				}
			}
		}
		
		await ctx.SaveChangesAsync();
	}

	public async Task<List<RoleEntity>> GetAll() {
		await using var ctx = databaseProvider.Provide();
		return await ctx.Roles.ToListAsync();
	}

	public async Task<ImmutableHashSet<string>> GetAllNames() {
		await using var ctx = databaseProvider.Provide();
		return await ctx.Roles.Select(static role => role.Name).AsAsyncEnumerable().ToImmutableSetAsync();
	}

	public async ValueTask<RoleEntity?> GetByGuid(Guid guid) {
		await using var ctx = databaseProvider.Provide();
		return await ctx.Roles.FindAsync(guid);
	}

	public async Task<Result<RoleEntity, AddRoleError>> Create(string name) {
		if (string.IsNullOrWhiteSpace(name)) {
			return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsEmpty);
		}
		else if (name.Length > MaxRoleNameLength) {
			return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameIsTooLong);
		}

		RoleEntity newRole;
		try {
			await using var ctx = databaseProvider.Provide();
			
			if (await ctx.Roles.AnyAsync(role => role.Name == name)) {
				return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.NameAlreadyExists);
			}
				
			newRole = new RoleEntity(Guid.NewGuid(), name);
			ctx.Roles.Add(newRole);
			await ctx.SaveChangesAsync();
		} catch (Exception e) {
			Logger.Error(e, "Could not create role \"{Name}\".", name);
			return Result.Fail<RoleEntity, AddRoleError>(AddRoleError.UnknownError);
		}

		Logger.Information("Created role \"{Name}\" (GUID {Guid}).", name, newRole.RoleGuid);
		return Result.Ok<RoleEntity, AddRoleError>(newRole);
	}
}