using Microsoft.EntityFrameworkCore; using RobotNet10.MapEditor.Shared.DTOs.Requests; using RobotNet10.MapManager.Data; namespace RobotNet10.MapManager.Services; /// /// Service implementation for managing layouts, versions, and levels /// public class LayoutService(MapDbContext context) : ILayoutService { private readonly MapDbContext _context = context; // ========================================== // LAYOUT OPERATIONS // ========================================== public async Task CreateLayoutAsync(CreateLayoutRequest request) { // Check if LayoutId already exists var exists = await _context.Layouts.AnyAsync(l => l.LayoutId == request.LayoutId); if (exists) { throw new InvalidOperationException($"Layout with ID '{request.LayoutId}' already exists"); } var layout = new Layout { LayoutId = request.LayoutId, LayoutName = request.LayoutName, Description = request.Description, IsActive = false, CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow, CreatedBy = request.CreatedBy }; _context.Layouts.Add(layout); await _context.SaveChangesAsync(); return layout; } public async Task> SearchLayoutsAsync(string? searchText) { var query = _context.Layouts.Include(l => l.Versions) .ThenInclude(v => v.Levels) .ThenInclude(l => l.EditorSettings) .AsSplitQuery() .AsQueryable(); if (!string.IsNullOrWhiteSpace(searchText)) { var search = searchText.ToLower(); query = query.Where(l => l.LayoutId.ToLower().Contains(search) || l.LayoutName.ToLower().Contains(search)); } return await query .OrderByDescending(l => l.ModifiedDate) .ToListAsync(); } public async Task GetLayoutByIdAsync(Guid layoutId) { return await _context.Layouts .Include(l => l.Versions) .ThenInclude(v => v.Levels) .ThenInclude(l => l.EditorSettings) .FirstOrDefaultAsync(l => l.Id == layoutId); } public async Task GetLayoutByLayoutIdAsync(string layoutId) { return await _context.Layouts .Include(l => l.Versions) .ThenInclude(v => v.Levels) .ThenInclude(l => l.EditorSettings) .FirstOrDefaultAsync(l => l.LayoutId == layoutId); } public async Task GetLayoutByNameAsync(string layoutName) { return await _context.Layouts .Include(l => l.Versions) .ThenInclude(v => v.Levels) .ThenInclude(l => l.EditorSettings) .FirstOrDefaultAsync(l => l.LayoutName == layoutName); } public async Task UpdateLayoutAsync(Guid layoutId, UpdateLayoutRequest request) { var layout = await GetLayoutByIdAsync(layoutId) ?? throw new InvalidOperationException($"Layout with ID '{layoutId}' not found"); layout.LayoutName = request.LayoutName; layout.Description = request.Description; layout.ModifiedDate = DateTime.UtcNow; layout.ModifiedBy = request.ModifiedBy; await _context.SaveChangesAsync(); return layout; } public async Task DeleteLayoutAsync(Guid layoutId) { var layout = await GetLayoutByIdAsync(layoutId); if (layout == null) { return false; } // Check if layout is active if (layout.IsActive) { throw new InvalidOperationException( $"Cannot delete active layout '{layout.LayoutId}'. Deactivate it first."); } // Hard delete - EF Core cascade will handle: // Layout → Versions → Levels → Nodes/Edges/Stations → Properties _context.Layouts.Remove(layout); await _context.SaveChangesAsync(); return true; } public async Task ActivateLayoutAsync(Guid layoutId) { var layout = await GetLayoutByIdAsync(layoutId) ?? throw new InvalidOperationException($"Layout with ID '{layoutId}' not found"); layout.IsActive = true; layout.ModifiedDate = DateTime.UtcNow; await _context.SaveChangesAsync(); return layout; } public async Task DeactivateLayoutAsync(Guid layoutId) { var layout = await GetLayoutByIdAsync(layoutId) ?? throw new InvalidOperationException($"Layout with ID '{layoutId}' not found"); layout.IsActive = false; layout.ModifiedDate = DateTime.UtcNow; await _context.SaveChangesAsync(); return layout; } // ========================================== // VERSION OPERATIONS // ========================================== public async Task CreateVersionAsync(Guid layoutId, CreateLayoutVersionRequest request) { var layout = await GetLayoutByIdAsync(layoutId) ?? throw new InvalidOperationException($"Layout with ID '{layoutId}' not found"); // Check if version already exists var exists = await _context.LayoutVersions .AnyAsync(v => v.LayoutId == layoutId && v.Version == request.Version); if (exists) { throw new InvalidOperationException( $"Version '{request.Version}' already exists for layout '{layout.LayoutId}'"); } var version = new LayoutVersion { LayoutId = layoutId, Version = request.Version, LayoutDescription = request.LayoutDescription, CreatedBy = request.CreatedBy, CreatedDate = DateTime.UtcNow, IsActive = false // New versions start as inactive }; _context.LayoutVersions.Add(version); await _context.SaveChangesAsync(); return version; } public async Task> GetVersionsAsync(Guid layoutId) { return await _context.LayoutVersions .Where(v => v.LayoutId == layoutId) .Include(v => v.Levels) .ThenInclude(l => l.EditorSettings) .OrderByDescending(v => v.CreatedDate) .ToListAsync(); } public async Task GetVersionAsync(Guid versionId) { return await _context.LayoutVersions .Include(v => v.Layout) .Include(v => v.Levels) .ThenInclude(l => l.EditorSettings) .FirstOrDefaultAsync(v => v.Id == versionId); } public async Task UpdateVersionAsync(Guid versionId, UpdateLayoutRequest request) { var version = await GetVersionAsync(versionId) ?? throw new InvalidOperationException($"Version with ID '{versionId}' not found"); version.LayoutDescription = request.Description; await _context.SaveChangesAsync(); return version; } public async Task DeleteVersionAsync(Guid versionId) { var version = await GetVersionAsync(versionId); if (version == null) { return false; } // Check if parent layout is active if (version.Layout.IsActive) { throw new InvalidOperationException( $"Cannot delete version from active layout '{version.Layout.LayoutId}'. " + "Deactivate the layout first."); } // Hard delete - cascade will handle levels and all nested data _context.LayoutVersions.Remove(version); await _context.SaveChangesAsync(); return true; } // ========================================== // LEVEL OPERATIONS // ========================================== public async Task CreateLevelAsync(Guid versionId, CreateLayoutLevelRequest request) { var version = await GetVersionAsync(versionId) ?? throw new InvalidOperationException($"Version with ID '{versionId}' not found"); // Check if level already exists var exists = await _context.LayoutLevels .AnyAsync(l => l.VersionId == versionId && l.LayoutLevelId == request.LayoutLevelId); if (exists) { throw new InvalidOperationException( $"Level '{request.LayoutLevelId}' already exists in this version"); } var level = new LayoutLevel { VersionId = versionId, LayoutLevelId = request.LayoutLevelId, LevelOrder = request.LevelOrder }; _context.LayoutLevels.Add(level); await _context.SaveChangesAsync(); // Create editor settings if coordinate system provided if (request.CoordinateSystem != null) { var settings = new LayoutLevelEditorSettings { LevelId = level.Id, OriginX = request.CoordinateSystem.OriginX, OriginY = request.CoordinateSystem.OriginY, Resolution = request.CoordinateSystem.Resolution, BoundsMinX = request.CoordinateSystem.BoundsMinX, BoundsMaxX = request.CoordinateSystem.BoundsMaxX, BoundsMinY = request.CoordinateSystem.BoundsMinY, BoundsMaxY = request.CoordinateSystem.BoundsMaxY, ImageWidth = request.CoordinateSystem.ImageWidth, ImageHeight = request.CoordinateSystem.ImageHeight, CreatedDate = DateTime.UtcNow, ModifiedDate = DateTime.UtcNow }; _context.LayoutLevelEditorSettings.Add(settings); await _context.SaveChangesAsync(); } return level; } public async Task> GetLevelsAsync(Guid versionId) { return await _context.LayoutLevels .Where(l => l.VersionId == versionId) .Include(l => l.EditorSettings) .OrderBy(l => l.LevelOrder) .ToListAsync(); } public async Task GetLevelAsync(Guid levelId) { return await _context.LayoutLevels .Include(l => l.Version) .ThenInclude(v => v.Layout) .Include(l => l.EditorSettings) .FirstOrDefaultAsync(l => l.Id == levelId); } public async Task UpdateLevelAsync(Guid levelId, UpdateLayoutLevelRequest request) { var level = await GetLevelAsync(levelId) ?? throw new InvalidOperationException($"Level with ID '{levelId}' not found"); // Update level properties if (request.LayoutLevelId != null) level.LayoutLevelId = request.LayoutLevelId; if (request.LevelOrder.HasValue) level.LevelOrder = request.LevelOrder.Value; // Get or create editor settings var settings = await _context.LayoutLevelEditorSettings .FirstOrDefaultAsync(s => s.LevelId == levelId); if (settings == null) { // Create new settings settings = new LayoutLevelEditorSettings { LevelId = levelId, CreatedDate = DateTime.UtcNow }; _context.LayoutLevelEditorSettings.Add(settings); } // Update coordinate system if provided if (request.CoordinateSystem != null) { settings.OriginX = request.CoordinateSystem.OriginX; settings.OriginY = request.CoordinateSystem.OriginY; settings.Resolution = request.CoordinateSystem.Resolution; settings.BoundsMinX = request.CoordinateSystem.BoundsMinX; settings.BoundsMaxX = request.CoordinateSystem.BoundsMaxX; settings.BoundsMinY = request.CoordinateSystem.BoundsMinY; settings.BoundsMaxY = request.CoordinateSystem.BoundsMaxY; settings.ImageWidth = request.CoordinateSystem.ImageWidth; settings.ImageHeight = request.CoordinateSystem.ImageHeight; } // Update editor settings if provided if (request.EditorSettings != null) { if (request.EditorSettings.EdgeMinLengthCreate.HasValue) settings.EdgeMinLengthCreate = request.EditorSettings.EdgeMinLengthCreate.Value; if (request.EditorSettings.EdgeNameAutoGenerate.HasValue) settings.EdgeNameAutoGenerate = request.EditorSettings.EdgeNameAutoGenerate.Value; if (request.EditorSettings.NodeNameAutoGenerate.HasValue) settings.NodeNameAutoGenerate = request.EditorSettings.NodeNameAutoGenerate.Value; if (request.EditorSettings.NodeProximityRadius.HasValue) settings.NodeProximityRadius = request.EditorSettings.NodeProximityRadius.Value; } // Update modified date if any settings were changed if (request.CoordinateSystem != null || request.EditorSettings != null) { settings.ModifiedDate = DateTime.UtcNow; } await _context.SaveChangesAsync(); return level; } public async Task DeleteLevelAsync(Guid levelId) { var level = await GetLevelAsync(levelId); if (level == null) { return false; } // Check if parent layout is active if (level.Version.Layout.IsActive) { throw new InvalidOperationException( $"Cannot delete level from active layout '{level.Version.Layout.LayoutId}'. " + "Deactivate the layout first."); } // Hard delete - cascade will handle all nested data _context.LayoutLevels.Remove(level); await _context.SaveChangesAsync(); return true; } }