using FlowAPI.Application.DTOs.Graph; using FlowAPI.Application.Interfaces; using FlowAPI.Domain.Entities; using System; using System.Collections.Generic; using System.Linq; using System.Text.Json; using System.Threading.Tasks; namespace FlowAPI.Application.Services { public class GraphService : IGraphService { private readonly IGraphRepository _repo; private readonly ITaskNodeRepository _nodeRepo; private readonly IEdgeRepository _edgeRepo; private readonly IAchievementService _achievementService; private readonly ISharedGraphRepository _sharedGraphRepo; private readonly IUserRepository _userRepo; private readonly ICustomTemplateRepository _customTemplateRepo; public GraphService( IGraphRepository repo, ITaskNodeRepository nodeRepo, IEdgeRepository edgeRepo, IAchievementService achievementService, ISharedGraphRepository sharedGraphRepo, IUserRepository userRepo, ICustomTemplateRepository customTemplateRepo) { _repo = repo; _nodeRepo = nodeRepo; _edgeRepo = edgeRepo; _achievementService = achievementService; _sharedGraphRepo = sharedGraphRepo; _userRepo = userRepo; _customTemplateRepo = customTemplateRepo; } public async Task> GetAllByUserIdAsync(Guid userId) { // Owned graphs var ownedGraphs = await _repo.GetAllByUserIdAsync(userId); var ownedDtos = ownedGraphs.Select(g => { var dto = MapToDto(g); dto.IsShared = false; dto.Permission = "Owner"; return dto; }).ToList(); // Shared graphs var sharedShares = await _sharedGraphRepo.GetSharedGraphsByUserIdAsync(userId); var sharedDtos = sharedShares.Select(s => { var dto = MapToDto(s.Graph); dto.IsShared = true; dto.Permission = s.Permission; return dto; }).ToList(); return ownedDtos.Concat(sharedDtos); } public async Task GetByIdAsync(Guid id, Guid currentUserId) { var graph = await _repo.GetByIdAsync(id); if (graph is null) return null; if (graph.UserId == currentUserId) { var dto = MapToDto(graph); dto.IsShared = false; dto.Permission = "Owner"; return dto; } var shared = await _sharedGraphRepo.GetShareAsync(id, currentUserId); if (shared is not null) { var dto = MapToDto(graph); dto.IsShared = true; dto.Permission = shared.Permission; return dto; } return null; } public async Task GetFullByIdAsync(Guid id, Guid currentUserId) { var graph = await _repo.GetByIdWithDetailsAsync(id); if (graph is null) return null; string permission = "Owner"; bool isShared = false; if (graph.UserId != currentUserId) { var shared = await _sharedGraphRepo.GetShareAsync(id, currentUserId); if (shared is null) return null; permission = shared.Permission; isShared = true; } return new FullGraphResponseDto { Id = graph.Id, UserId = graph.UserId, Title = graph.Title, Description = graph.Description, CreatedAt = graph.CreatedAt, IsShared = isShared, Permission = permission, Nodes = graph.Nodes.Select(n => { bool hasLarge = n.Data != null && n.Data.Length > 2000; return new NodeDto { Id = n.Id, Label = n.Label, PosX = n.PosX, PosY = n.PosY, Type = n.Type, State = (int)n.State, Decision = n.Decision, Description = n.Description, Color = n.Color, IsPinned = n.IsPinned, Width = n.Width, Height = n.Height, Data = hasLarge ? null : n.Data, HasLargeData = hasLarge }; }).ToList(), Edges = graph.Edges.Select(e => new EdgeDto { Id = e.Id, FromNodeId = e.FromNodeId, ToNodeId = e.ToNodeId, Condition = e.Condition.ToString() }).ToList() }; } public async Task CreateAsync(CreateGraphDto dto, Guid currentUserId) { var user = await _userRepo.GetByIdAsync(currentUserId); if (user == null) throw new Exception("User not found."); var tier = user.SubscriptionTier?.Trim(); bool isFree = string.IsNullOrEmpty(tier) || tier == "Free"; if (isFree) { var ownedGraphsCount = (await _repo.GetAllByUserIdAsync(currentUserId)).Count(); if (ownedGraphsCount >= 3) { throw new InvalidOperationException("Free tier is limited to 3 canvases."); } } var graph = new Graph { Id = Guid.NewGuid(), UserId = currentUserId, // Secure: always force authenticated user ID Title = dto.Title, Description = dto.Description, CreatedAt = DateTime.UtcNow }; if (!string.IsNullOrEmpty(dto.TemplateKey)) { // Try to load system template first var (nodes, edges) = TemplateLibrary.GetTemplateContent(dto.TemplateKey, graph.Id); // If not found in system templates, check custom templates in DB if (nodes.Count == 0 && Guid.TryParse(dto.TemplateKey, out var customTemplateId)) { var customTemplate = await _customTemplateRepo.GetByIdAsync(customTemplateId); if (customTemplate != null && (customTemplate.UserId == currentUserId || user.SubscriptionTier == "Ultra")) { try { var data = JsonSerializer.Deserialize(customTemplate.ContentJson); if (data != null) { var idMapping = new Dictionary(); foreach (var nodeDto in data.Nodes) { var newId = Guid.NewGuid(); idMapping[nodeDto.Id] = newId; var node = new TaskNode { Id = newId, GraphId = graph.Id, Label = nodeDto.Label, PosX = nodeDto.PosX, PosY = nodeDto.PosY, Type = nodeDto.Type, State = FlowAPI.Domain.Enums.NodeState.Pending, CreatedAt = DateTime.UtcNow, Description = nodeDto.Description, Color = nodeDto.Color, IsPinned = nodeDto.IsPinned, Width = nodeDto.Width, Height = nodeDto.Height, Data = nodeDto.Data, Decision = nodeDto.Decision }; nodes.Add(node); } foreach (var edgeDto in data.Edges) { if (idMapping.TryGetValue(edgeDto.FromNodeId, out var fromId) && idMapping.TryGetValue(edgeDto.ToNodeId, out var toId)) { edges.Add(new Edge { Id = Guid.NewGuid(), GraphId = graph.Id, FromNodeId = fromId, ToNodeId = toId, Condition = edgeDto.Condition }); } } } } catch (Exception ex) { System.Console.WriteLine($"[GraphService] Failed to load custom template: {ex.Message}"); } } } if (nodes.Count > 0) { graph.Nodes = nodes; graph.Edges = edges; if (string.IsNullOrEmpty(dto.Description) || dto.Description == "New strategy path...") { graph.Description = $"Побудовано за шаблоном"; } } } var created = await _repo.CreateAsync(graph); await _achievementService.ProcessEventAsync(currentUserId, "GraphCreated"); if (graph.Nodes.Count > 0) { await _achievementService.ProcessEventAsync(currentUserId, "NodeCreated", graph.Nodes.Count); } if (graph.Edges.Count > 0) { await _achievementService.ProcessEventAsync(currentUserId, "EdgeCreated", graph.Edges.Count); } var result = MapToDto(created); result.IsShared = false; result.Permission = "Owner"; return result; } public async Task UpdateAsync(Guid id, UpdateGraphDto dto, Guid currentUserId) { var graph = await _repo.GetByIdAsync(id); if (graph is null) return null; // Secure: only allowed for owner or edit shared users if (graph.UserId != currentUserId) { var shared = await _sharedGraphRepo.GetShareAsync(id, currentUserId); if (shared is null || shared.Permission != "Edit") return null; } if (dto.Title is not null) graph.Title = dto.Title; if (dto.Description is not null) graph.Description = dto.Description; var updated = await _repo.UpdateAsync(graph); var result = MapToDto(updated); if (graph.UserId != currentUserId) { result.IsShared = true; result.Permission = "Edit"; } return result; } public async Task DeleteAsync(Guid id, Guid currentUserId) { var graph = await _repo.GetByIdAsync(id); if (graph == null) return false; // Secure: only owner can delete the graph if (graph.UserId != currentUserId) return false; var deleted = await _repo.DeleteAsync(id); if (deleted) { await _achievementService.ProcessEventAsync(graph.UserId, "GraphDeleted"); } return deleted; } public async Task SaveFullGraphAsync(Guid id, FullGraphResponseDto dto, Guid currentUserId) { var graph = await _repo.GetByIdWithDetailsAsync(id); if (graph is null) return false; var user = await _userRepo.GetByIdAsync(currentUserId); if (user == null) throw new Exception("User not found."); var tier = user.SubscriptionTier?.Trim(); bool isFree = string.IsNullOrEmpty(tier) || tier == "Free"; if (isFree) { if (dto.Nodes.Count > 200) { throw new InvalidOperationException("Free tier is limited to 200 nodes per canvas."); } } // Secure: only allowed for owner or shared users with Edit permission if (graph.UserId != currentUserId) { var shared = await _sharedGraphRepo.GetShareAsync(id, currentUserId); if (shared is null || shared.Permission != "Edit") return false; } // Map DTOs to Domain Entities var mappedNodes = dto.Nodes.Select(nodeDto => new TaskNode { Id = nodeDto.Id == Guid.Empty ? Guid.NewGuid() : nodeDto.Id, GraphId = id, Label = nodeDto.Label, PosX = nodeDto.PosX, PosY = nodeDto.PosY, Type = nodeDto.Type, Decision = nodeDto.Decision, State = (FlowAPI.Domain.Enums.NodeState)nodeDto.State, Description = nodeDto.Description, Color = nodeDto.Color, IsPinned = nodeDto.IsPinned, Width = nodeDto.Width, Height = nodeDto.Height, Data = nodeDto.Data }).ToList(); var mappedEdges = dto.Edges.Select(edgeDto => new Edge { Id = edgeDto.Id == Guid.Empty ? Guid.NewGuid() : edgeDto.Id, GraphId = id, FromNodeId = edgeDto.FromNodeId, ToNodeId = edgeDto.ToNodeId, Condition = edgeDto.Condition ?? "True" }).ToList(); // Calculate deltas for achievements var oldNodes = graph.Nodes ?? new List(); var oldEdges = graph.Edges ?? new List(); int newlyCreatedNodes = mappedNodes.Count(n => !oldNodes.Any(o => o.Id == n.Id)); int newlyCreatedEdges = mappedEdges.Count(e => !oldEdges.Any(o => o.Id == e.Id)); int newlyCompletedNodes = mappedNodes.Count(n => n.State == FlowAPI.Domain.Enums.NodeState.Done && (!oldNodes.Any(o => o.Id == n.Id) || oldNodes.First(o => o.Id == n.Id).State != FlowAPI.Domain.Enums.NodeState.Done) ); // Perform robust Upsert var success = await _repo.UpsertFullGraphAsync(id, mappedNodes, mappedEdges); if (!success) return false; // Trigger Achievements based on deltas if (newlyCreatedNodes > 0) await _achievementService.ProcessEventAsync(graph.UserId, "NodeCreated", newlyCreatedNodes); if (newlyCreatedEdges > 0) await _achievementService.ProcessEventAsync(graph.UserId, "EdgeCreated", newlyCreatedEdges); if (newlyCompletedNodes > 0) await _achievementService.ProcessEventAsync(graph.UserId, "TaskCompleted", newlyCompletedNodes); return true; } public async Task ShareGraphAsync(Guid graphId, Guid ownerId, ShareGraphRequestDto dto) { var graph = await _repo.GetByIdAsync(graphId); if (graph is null || graph.UserId != ownerId) return null; var owner = await _userRepo.GetByIdAsync(ownerId); if (owner == null) throw new Exception("Owner user not found."); var targetUser = await _userRepo.GetByEmailAsync(dto.Email.ToLower().Trim()); if (targetUser is null || targetUser.Id == ownerId) return null; var existing = await _sharedGraphRepo.GetShareAsync(graphId, targetUser.Id); if (existing is not null) { existing.Permission = dto.Permission; await _sharedGraphRepo.UpdateAsync(existing); } else { // Enforce sharing tier limits based on owner's tier var shares = await _sharedGraphRepo.GetSharesForGraphAsync(graphId); int currentSharedCount = shares?.Count() ?? 0; var ownerTier = owner.SubscriptionTier?.Trim(); bool ownerIsFree = string.IsNullOrEmpty(ownerTier) || ownerTier == "Free"; if (ownerIsFree && currentSharedCount >= 1) { throw new InvalidOperationException("Free tier is limited to 2 people on a board (1 guest)."); } else if (ownerTier == "Pro" && currentSharedCount >= 9) { throw new InvalidOperationException("Pro tier is limited to 10 people on a board (9 guests)."); } var share = new SharedGraph { Id = Guid.NewGuid(), GraphId = graphId, SharedWithUserId = targetUser.Id, Permission = dto.Permission, CreatedAt = DateTime.UtcNow }; await _sharedGraphRepo.CreateAsync(share); } var res = MapToDto(graph); res.IsShared = true; res.Permission = dto.Permission; return res; } public async Task> GetSharedUsersAsync(Guid graphId, Guid ownerId) { var graph = await _repo.GetByIdAsync(graphId); if (graph is null) return Enumerable.Empty(); // Allow access to shares list if caller is the owner or a guest with access if (graph.UserId != ownerId) { var shareAccess = await _sharedGraphRepo.GetShareAsync(graphId, ownerId); if (shareAccess is null) return Enumerable.Empty(); } var result = new List(); // Get owner details var owner = await _userRepo.GetByIdAsync(graph.UserId); if (owner is not null) { result.Add(new SharedUserResponseDto { Id = Guid.Empty, SharedWithUserId = owner.Id, Email = owner.Email, DisplayName = owner.DisplayName, Permission = "Owner" }); } // Get shared users var shares = await _sharedGraphRepo.GetSharesForGraphAsync(graphId); if (shares is not null) { result.AddRange(shares.Select(s => new SharedUserResponseDto { Id = s.Id, SharedWithUserId = s.SharedWithUserId, Email = s.SharedWithUser?.Email ?? string.Empty, DisplayName = s.SharedWithUser?.DisplayName ?? string.Empty, Permission = s.Permission })); } return result; } public async Task RevokeShareAsync(Guid graphId, Guid ownerId, Guid shareId) { var graph = await _repo.GetByIdAsync(graphId); if (graph is null || graph.UserId != ownerId) return false; var share = await _sharedGraphRepo.GetByIdAsync(shareId); if (share is null || share.GraphId != graphId) return false; return await _sharedGraphRepo.DeleteAsync(shareId); } private static GraphResponseDto MapToDto(Graph graph) => new() { Id = graph.Id, UserId = graph.UserId, Title = graph.Title, Description = graph.Description, CreatedAt = graph.CreatedAt }; public async Task SaveAsTemplateAsync(Guid graphId, Guid userId, string title, string description) { var user = await _userRepo.GetByIdAsync(userId); if (user == null) throw new Exception("User not found."); if (user.SubscriptionTier != "Ultra") { throw new InvalidOperationException("Creating custom templates is an Ultra tier feature."); } var graph = await _repo.GetByIdWithDetailsAsync(graphId); if (graph == null || graph.UserId != userId) { throw new Exception("Graph not found or you are not the owner."); } // Map nodes and edges to DTOs for serialization var nodes = graph.Nodes.Select(n => new NodeDto { Id = n.Id, Label = n.Label, PosX = n.PosX, PosY = n.PosY, Type = n.Type, State = (int)n.State, Decision = n.Decision, Description = n.Description, Color = n.Color, IsPinned = n.IsPinned, Width = n.Width, Height = n.Height, Data = n.Data }).ToList(); var edges = graph.Edges.Select(e => new EdgeDto { Id = e.Id, FromNodeId = e.FromNodeId, ToNodeId = e.ToNodeId, Condition = e.Condition }).ToList(); var contentJson = JsonSerializer.Serialize(new CustomTemplateContentDto { Nodes = nodes, Edges = edges }); var template = new CustomTemplate { Id = Guid.NewGuid(), UserId = userId, Title = title, Description = description, Category = "Custom", Icon = "BookOpen", ContentJson = contentJson, CreatedAt = DateTime.UtcNow }; return await _customTemplateRepo.CreateAsync(template); } public async Task> GetCustomTemplatesAsync(Guid userId) { return await _customTemplateRepo.GetByUserIdAsync(userId); } private class CustomTemplateContentDto { public List Nodes { get; set; } = new(); public List Edges { get; set; } = new(); } } }