using System.Globalization; using System.IO; using System.Text; using System.Text.RegularExpressions; using CsvHelper; using CsvHelper.Configuration; using SharepointToolbox.Core.Models; namespace SharepointToolbox.Services; public class CsvValidationService : ICsvValidationService { private static readonly Regex EmailRegex = new( @"^[^@\s]+@[^@\s]+\.[^@\s]+$", RegexOptions.Compiled); public List> ParseAndValidate(Stream csvStream) where T : class { using var reader = new StreamReader(csvStream, Encoding.UTF8, detectEncodingFromByteOrderMarks: true); using var csv = new CsvReader(reader, new CsvConfiguration(CultureInfo.InvariantCulture) { HasHeaderRecord = true, MissingFieldFound = null, HeaderValidated = null, DetectDelimiter = true, TrimOptions = TrimOptions.Trim, }); var rows = new List>(); csv.Read(); csv.ReadHeader(); while (csv.Read()) { try { var record = csv.GetRecord(); if (record == null) { rows.Add(CsvValidationRow.ParseError(csv.Context.Parser.RawRecord, "Failed to parse row")); continue; } rows.Add(new CsvValidationRow(record, new List())); } catch (Exception ex) { rows.Add(CsvValidationRow.ParseError(csv.Context.Parser.RawRecord, ex.Message)); } } return rows; } public List> ParseAndValidateMembers(Stream csvStream) { var rows = ParseAndValidate(csvStream); foreach (var row in rows.Where(r => r.IsValid && r.Record != null)) { var errors = ValidateMemberRow(row.Record!); row.Errors.AddRange(errors); } return rows; } public List> ParseAndValidateSites(Stream csvStream) { var rows = ParseAndValidate(csvStream); foreach (var row in rows.Where(r => r.IsValid && r.Record != null)) { var errors = ValidateSiteRow(row.Record!); row.Errors.AddRange(errors); } return rows; } public List> ParseAndValidateFolders(Stream csvStream) { var rows = ParseAndValidate(csvStream); foreach (var row in rows.Where(r => r.IsValid && r.Record != null)) { var errors = ValidateFolderRow(row.Record!); row.Errors.AddRange(errors); } return rows; } private static List ValidateMemberRow(BulkMemberRow row) { var errors = new List(); if (string.IsNullOrWhiteSpace(row.Email)) errors.Add("Email is required"); else if (!EmailRegex.IsMatch(row.Email.Trim())) errors.Add($"Invalid email format: {row.Email}"); if (string.IsNullOrWhiteSpace(row.GroupName) && string.IsNullOrWhiteSpace(row.GroupUrl)) errors.Add("GroupName or GroupUrl is required"); if (!string.IsNullOrWhiteSpace(row.Role) && !row.Role.Equals("Member", StringComparison.OrdinalIgnoreCase) && !row.Role.Equals("Owner", StringComparison.OrdinalIgnoreCase)) errors.Add($"Role must be 'Member' or 'Owner', got: {row.Role}"); return errors; } private static List ValidateSiteRow(BulkSiteRow row) { var errors = new List(); if (string.IsNullOrWhiteSpace(row.Name)) errors.Add("Name is required"); if (string.IsNullOrWhiteSpace(row.Type)) errors.Add("Type is required"); else if (!row.Type.Equals("Team", StringComparison.OrdinalIgnoreCase) && !row.Type.Equals("Communication", StringComparison.OrdinalIgnoreCase)) errors.Add($"Type must be 'Team' or 'Communication', got: {row.Type}"); // Team sites require at least one owner (Pitfall 6 from research) if (row.Type.Equals("Team", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(row.Owners)) errors.Add("Team sites require at least one owner"); // Team sites need an alias if (row.Type.Equals("Team", StringComparison.OrdinalIgnoreCase) && string.IsNullOrWhiteSpace(row.Alias)) errors.Add("Team sites require an alias"); return errors; } private static List ValidateFolderRow(FolderStructureRow row) { var errors = new List(); if (string.IsNullOrWhiteSpace(row.Level1)) errors.Add("Level1 is required (root folder)"); return errors; } }