using PlotLine.Data; using PlotLine.Models; namespace PlotLine.Services; public sealed class AccountDeletionService( IUserRepository users, IProjectRepository projects, ISubscriptionService subscriptions, IHardDeleteFileCleanupService fileCleanup, ILogger logger) : IAccountDeletionService { public async Task HardDeleteAccountAsync(int userId) { var user = await users.GetByIdAsync(userId); if (user is null) { logger.LogWarning("Account hard delete requested for missing user {UserID}.", userId); return AccountDeletionResult.MissingUser(); } if (await subscriptions.HasPaidBillingRiskAsync(userId)) { logger.LogWarning("Account hard delete blocked for user {UserID}: paid billing risk exists.", userId); return AccountDeletionResult.PaidSubscriptionBlocked(); } var deletedProjects = 0; var deletedFiles = 0; var missingFiles = 0; var failedFiles = 0; var ownedProjects = await projects.ListOwnedForAccountDeletionAsync(userId); logger.LogInformation("Account hard delete started for user {UserID} with {OwnedProjectCount} owned projects.", userId, ownedProjects.Count); foreach (var project in ownedProjects) { IReadOnlyList projectFilePaths; try { projectFilePaths = await projects.ListHardDeleteFilePathsForAccountDeletionAsync(project.ProjectID, userId); await projects.HardDeleteForAccountDeletionAsync(project.ProjectID, userId); deletedProjects++; logger.LogInformation("Account hard delete removed project {ProjectID} for user {UserID}. {FilePathCount} file paths queued for cleanup.", project.ProjectID, userId, projectFilePaths.Count); } catch (Exception ex) { logger.LogError(ex, "Account hard delete failed while deleting project {ProjectID} for user {UserID}. Account deletion stopped.", project.ProjectID, userId); return AccountDeletionResult.Failure("The account could not be deleted. No further account data was removed."); } var projectCleanup = fileCleanup.DeleteFiles("account project hard delete", project.ProjectID, projectFilePaths); deletedFiles += projectCleanup.Deleted; missingFiles += projectCleanup.Missing; failedFiles += projectCleanup.Failed; } IReadOnlyList userFilePaths; try { userFilePaths = await users.ListHardDeleteFilePathsAsync(userId); } catch (Exception ex) { logger.LogError(ex, "Account hard delete could not list remaining user-level files for user {UserID}. Account deletion stopped.", userId); return AccountDeletionResult.Failure("The account could not be deleted. No further account data was removed."); } var userCleanup = fileCleanup.DeleteFiles("account user file hard delete", userId, userFilePaths); deletedFiles += userCleanup.Deleted; missingFiles += userCleanup.Missing; failedFiles += userCleanup.Failed; try { var deleted = await users.HardDeleteAccountDataAsync(userId); if (!deleted) { logger.LogError("Account hard delete failed to remove the AppUser row for user {UserID}.", userId); return AccountDeletionResult.Failure("The account could not be deleted."); } } catch (Exception ex) { logger.LogError(ex, "Account hard delete failed while removing remaining user data for user {UserID}.", userId); return AccountDeletionResult.Failure("The account could not be deleted."); } if (failedFiles > 0) { logger.LogWarning("Account hard delete completed for user {UserID}, but file cleanup had {FailedFileCount} failures, {DeletedFileCount} deleted files, and {MissingFileCount} missing files.", userId, failedFiles, deletedFiles, missingFiles); } else { logger.LogInformation("Account hard delete completed for user {UserID}: {DeletedProjectCount} projects, {DeletedFileCount} files deleted, {MissingFileCount} files missing.", userId, deletedProjects, deletedFiles, missingFiles); } return AccountDeletionResult.Success(deletedProjects, deletedFiles, missingFiles, failedFiles); } }