using System.Security.Claims; using FastEndpoints; using Microsoft.EntityFrameworkCore; using TrackQrApi.Data; using TrackQrApi.Features.Auth.Common; using TrackQrApi.Features.Links.Common; using TrackQrApi.Models; namespace TrackQrApi.Features.Links.Endpoints; public class BulkCreateLinksRequest { public Guid WorkspaceId { get; set; } public required List Links { get; set; } } public class BulkLinkItem { public required string DestinationUrl { get; set; } public string? Title { get; set; } public string? Slug { get; set; } } public class BulkCreateLinksResponse { public required List Created { get; set; } public required List Errors { get; set; } } public class BulkLinkError { public int Index { get; set; } public required string Url { get; set; } public required string Error { get; set; } } public class BulkCreateLinksEndpoint(AppDbContext db) : Endpoint { public override void Configure() { Post("/workspaces/{WorkspaceId}/links/bulk"); } public override async Task HandleAsync(BulkCreateLinksRequest req, CancellationToken ct) { var userId = Guid.Parse(User.FindFirstValue(ClaimTypes.NameIdentifier)!); // Verify workspace ownership var workspace = await db.Workspaces .FirstOrDefaultAsync(w => w.Id == req.WorkspaceId && w.OwnerUserId == userId, ct); if (workspace is null) { await HttpContext.Response.SendAsync(new MessageResponse("Workspace not found"), 404, cancellation: ct); return; } // Limit bulk creation to 100 links at a time if (req.Links.Count > 100) { await HttpContext.Response.SendAsync(new MessageResponse("Maximum 100 links per request"), 400, cancellation: ct); return; } var created = new List(); var errors = new List(); // Check for plan limits var currentLinkCount = await db.ShortLinks.CountAsync(l => l.WorkspaceId == req.WorkspaceId, ct); var linkLimit = GetPlanLinkLimit(workspace.Plan); for (var i = 0; i < req.Links.Count; i++) { var item = req.Links[i]; // Validate URL if (!Uri.TryCreate(item.DestinationUrl, UriKind.Absolute, out var uri) || (uri.Scheme != "http" && uri.Scheme != "https")) { errors.Add(new BulkLinkError { Index = i, Url = item.DestinationUrl, Error = "Invalid URL" }); continue; } // Check plan limits if (linkLimit.HasValue && currentLinkCount + created.Count >= linkLimit.Value) { errors.Add(new BulkLinkError { Index = i, Url = item.DestinationUrl, Error = "Plan link limit reached" }); continue; } // Generate or validate slug var slug = item.Slug; if (string.IsNullOrWhiteSpace(slug)) { slug = GenerateSlug(); } else { // Check if slug is taken var slugTaken = await db.ShortLinks.AnyAsync(l => l.Slug == slug, ct); if (slugTaken) { errors.Add(new BulkLinkError { Index = i, Url = item.DestinationUrl, Error = $"Slug '{slug}' is already taken" }); continue; } } var link = new ShortLink { Id = Guid.NewGuid(), WorkspaceId = req.WorkspaceId, Slug = slug, DestinationUrl = item.DestinationUrl, Title = item.Title, Status = ShortLinkStatus.Active, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; db.ShortLinks.Add(link); created.Add(new LinkDto { Id = link.Id, Slug = link.Slug, DestinationUrl = link.DestinationUrl, Title = link?.Title, Status = link.Status.ToString(), ClickCount = 0, CreatedAt = link.CreatedAt }); } await db.SaveChangesAsync(ct); var response = new BulkCreateLinksResponse { Created = created, Errors = errors }; await HttpContext.Response.SendAsync(response, 201, cancellation: ct); } private static string GenerateSlug() { const string chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; var random = new Random(); return new string(Enumerable.Repeat(chars, 7).Select(s => s[random.Next(s.Length)]).ToArray()); } private static int? GetPlanLinkLimit(WorkspacePlan? plan) { return plan switch { WorkspacePlan.Business => null, // Unlimited WorkspacePlan.Pro => 10000, _ => 100 // Free plan }; } }