using System.Net.Mail; using System.Security.Claims; using Data.SeaHavenIndustries; using Data.SeaHavenIndustries.Enums; using Microsoft.AspNetCore.Identity; using SeaHaven.DataServices.Interfaces; using SeaHaven.Services.Constants; using SeaHaven.Services.DTOs; using SeaHaven.Services.Interfaces; namespace SeaHaven.Services.Implementation; public sealed class TeamMemberService : ITeamMemberService { private const string ActiveStatus = "Active"; private readonly UserManager _userManager; private readonly RoleManager _roleManager; private readonly IUserServiceAreaDataService _areaDataService; private readonly ITeamPermissionOverrideDataService _permissionDataService; public TeamMemberService( UserManager userManager, RoleManager roleManager, IUserServiceAreaDataService areaDataService, ITeamPermissionOverrideDataService permissionDataService) { _userManager = userManager; _roleManager = roleManager; _areaDataService = areaDataService; _permissionDataService = permissionDataService; } public async Task CreateAsync( CreateTeamMemberRequestDTO request, ClaimsPrincipal caller, CancellationToken cancellationToken) { if (caller?.IsInRole("Admin") != true) return Failure("Forbidden"); var validation = Validate(request, out var role, out var color, out var email, out var areas, out var overrides); if (validation is not null) return Failure(validation); if (await _userManager.FindByEmailAsync(email!) is not null) return Failure("Email is already in use."); if (!await _roleManager.RoleExistsAsync(role!)) { var roleResult = await _roleManager.CreateAsync(new IdentityRole(role!)); if (!roleResult.Succeeded) return Failure(roleResult.Errors.FirstOrDefault()?.Description ?? "Unable to create role."); } var now = DateTime.UtcNow; var user = new ApplicationUser { UserName = email, Email = email, EmailConfirmed = false, FirstName = request.Name!.Trim(), Contact = request.Phone?.Trim(), PhoneNumber = request.Phone?.Trim(), Color = color, UniqueName = ActiveStatus, CreatedDate = now, PendingRegistration = true, PendingRegistrationCreatedDate = now }; var createResult = await _userManager.CreateAsync(user); if (!createResult.Succeeded) return Failure(createResult.Errors.FirstOrDefault()?.Description ?? "Unable to create team member."); var addRoleResult = await _userManager.AddToRoleAsync(user, role!); if (!addRoleResult.Succeeded) { await _userManager.DeleteAsync(user); return Failure(addRoleResult.Errors.FirstOrDefault()?.Description ?? "Unable to assign role."); } try { await _areaDataService.ReplaceAsync(user.Id, areas!, cancellationToken); await _permissionDataService.SetOverridesAsync(user.Id, overrides!, cancellationToken); } catch { await _userManager.DeleteAsync(user); throw; } return new CreateTeamMemberOutcomeDTO { Success = true, Member = new TeamMemberCreatedDTO { Id = user.Id, Name = user.FirstName!, Role = role!, Color = color!, Email = user.Email, Phone = user.PhoneNumber, ServiceAreas = areas!, PendingRegistration = true } }; } private static string? Validate( CreateTeamMemberRequestDTO request, out string? role, out string? color, out string? email, out IReadOnlyList? areas, out IReadOnlyDictionary? overrides) { role = TeamMemberConstants.CanonicalRole(request.Role); color = request.Color?.Trim(); email = request.Email?.Trim(); areas = NormalizeAreas(request.ServiceAreas, out var invalidArea); overrides = NormalizeOverrides(request.PermissionOverrides, out var invalidPermission); if (string.IsNullOrWhiteSpace(request.Name)) return "Name is required."; if (role is null) return "Role must be Dispatcher, Scheduler, or Admin."; if (color is null || !TeamMemberConstants.Colors.Contains(color)) return "Color must be selected from the accessible palette."; if (!IsValidEmail(email)) return "A valid email is required."; if (invalidArea is not null) return $"Unknown service area: {invalidArea}."; if (string.Equals(role, "Dispatcher", StringComparison.Ordinal) && (areas is null || areas.Count == 0)) return "At least one service area is required for a Dispatcher."; if (!string.Equals(role, "Dispatcher", StringComparison.Ordinal) && areas is { Count: > 0 }) return "Service areas are only available for Dispatchers."; if (invalidPermission is not null) return $"Unknown permission key: {invalidPermission}."; return null; } private static IReadOnlyList NormalizeAreas( IReadOnlyList? values, out string? invalidArea) { invalidArea = null; var normalized = new List(); foreach (var value in values ?? Array.Empty()) { var canonical = TeamMemberConstants.CanonicalArea(value); if (canonical is null) { invalidArea = value; return Array.Empty(); } if (!normalized.Contains(canonical, StringComparer.Ordinal)) normalized.Add(canonical); } return normalized; } private static IReadOnlyDictionary NormalizeOverrides( IReadOnlyDictionary? values, out string? invalidPermission) { invalidPermission = null; var normalized = new Dictionary(StringComparer.OrdinalIgnoreCase); foreach (var pair in values ?? new Dictionary()) { var canonical = TeamPermissionKeys.All.FirstOrDefault(key => string.Equals(key, pair.Key, StringComparison.OrdinalIgnoreCase)); if (canonical is null) { invalidPermission = pair.Key; return new Dictionary(); } if (!Enum.IsDefined(pair.Value)) { invalidPermission = pair.Key; return new Dictionary(); } normalized[canonical] = pair.Value; } return normalized; } private static bool IsValidEmail(string? value) { if (string.IsNullOrWhiteSpace(value)) return false; try { var address = new MailAddress(value); return string.Equals(address.Address, value, StringComparison.OrdinalIgnoreCase); } catch (FormatException) { return false; } } private static CreateTeamMemberOutcomeDTO Failure(string error) => new() { Success = false, Error = error }; }