using Core.Entities; using Core.Services; using Data; using Microsoft.EntityFrameworkCore; using System.Security.Claims; namespace WebApp.Services; public class NotesService : INotesService { private readonly AppDbContext _context; private readonly IHttpContextAccessor _httpContextAccessor; private readonly ILogger _logger; private readonly INoteNamingService _noteNamingService; public NotesService( AppDbContext context, IHttpContextAccessor httpContextAccessor, ILogger logger, INoteNamingService noteNamingService) { _context = context; _httpContextAccessor = httpContextAccessor; _logger = logger; _noteNamingService = noteNamingService; } private string? GetCurrentUserEmail() { var user = _httpContextAccessor.HttpContext?.User; if (user == null) return null; return user.FindFirstValue(ClaimTypes.Email) ?? user.Identity?.Name; } public async Task> GetNotesAsync(bool includeDeleted = false) { var query = _context.Notes .AsNoTracking() .AsQueryable(); if (!includeDeleted) { query = query.Where(n => !n.IsDeleted); } return await query .OrderBy(n => n.Title != null && n.Title.StartsWith("#") ? 1 : 0) // Non-page notes first (0), page notes last (1) .ThenByDescending(n => n.UpdatedAt) // Within each group, order by most recently updated .ToListAsync(); } public async Task GetNoteAsync(int id) { return await _context.Notes .Include(n => n.NoteHistories.OrderByDescending(h => h.ModifiedAt)) .FirstOrDefaultAsync(n => n.Id == id); } public async Task GetPageNoteAsync(string pageIdentifier) { var pageNoteTitle = _noteNamingService.GetPageNoteTitle(pageIdentifier); return await _context.Notes .AsNoTracking() .Where(n => n.Title == pageNoteTitle && !n.IsDeleted) .FirstOrDefaultAsync(); } public async Task> GetNoteHistoryAsync(int noteId) { return await _context.NoteHistories .AsNoTracking() .Where(h => h.NoteId == noteId) .OrderByDescending(h => h.ModifiedAt) .Take(10) .ToListAsync(); } public async Task CreateNoteAsync(Note note) { var userEmail = GetCurrentUserEmail(); var now = DateTime.UtcNow; note.CreatedAt = now; note.UpdatedAt = now; note.CreatedBy = userEmail; note.LastModifiedBy = userEmail; _context.Notes.Add(note); await _context.SaveChangesAsync(); // Create initial history entry var history = new NoteHistory { NoteId = note.Id, Title = note.Title, Content = note.Content, ModifiedBy = userEmail, ModifiedAt = now, ChangeType = "Created" }; _context.NoteHistories.Add(history); await _context.SaveChangesAsync(); _logger.LogInformation("Note created: {NoteId} by {User}", note.Id, userEmail); return note; } public async Task UpdateNoteAsync(Note note) { var existingNote = await _context.Notes .FirstOrDefaultAsync(n => n.Id == note.Id); if (existingNote == null) { throw new InvalidOperationException($"Note with ID {note.Id} not found."); } var userEmail = GetCurrentUserEmail(); var now = DateTime.UtcNow; // Create history entry with previous state before updating var history = new NoteHistory { NoteId = existingNote.Id, Title = existingNote.Title, Content = existingNote.Content, ModifiedBy = userEmail, ModifiedAt = now, ChangeType = "Updated" }; _context.NoteHistories.Add(history); // Update the note existingNote.Title = note.Title; existingNote.Content = note.Content; existingNote.UpdatedAt = now; existingNote.LastModifiedBy = userEmail; await _context.SaveChangesAsync(); _logger.LogInformation("Note updated: {NoteId} by {User}", note.Id, userEmail); return existingNote; } public async Task DeleteNoteAsync(int id) { var note = await _context.Notes .FirstOrDefaultAsync(n => n.Id == id); if (note == null) { throw new InvalidOperationException($"Note with ID {id} not found."); } var userEmail = GetCurrentUserEmail(); var now = DateTime.UtcNow; // Create deletion history entry var history = new NoteHistory { NoteId = note.Id, Title = note.Title, Content = note.Content, ModifiedBy = userEmail, ModifiedAt = now, ChangeType = "Soft Deleted" }; _context.NoteHistories.Add(history); // Soft delete - set IsDeleted flag instead of removing note.IsDeleted = true; note.UpdatedAt = now; note.LastModifiedBy = userEmail; await _context.SaveChangesAsync(); _logger.LogInformation("Note soft deleted: {NoteId} by {User}", id, userEmail); } public async Task> GetPinnedNotesAsync() { return await _context.Notes .AsNoTracking() .Where(n => n.IsPinned && (n.Title == null || !n.Title.StartsWith("#")) && !n.IsDeleted) .OrderByDescending(n => n.UpdatedAt) .Take(3) .ToListAsync(); } public async Task TogglePinNoteAsync(int noteId) { var note = await _context.Notes .FirstOrDefaultAsync(n => n.Id == noteId); if (note == null) { throw new InvalidOperationException($"Note with ID {noteId} not found."); } // Prevent pinning page notes if (_noteNamingService.IsPageNote(note.Title)) { throw new InvalidOperationException("Page notes cannot be pinned."); } var userEmail = GetCurrentUserEmail(); var now = DateTime.UtcNow; // If pinning and already 3 pinned notes exist (excluding this note if it's already pinned) if (!note.IsPinned) { var pinnedCount = await _context.Notes .CountAsync(n => n.IsPinned && (n.Title == null || !n.Title.StartsWith("#")) && !n.IsDeleted); if (pinnedCount >= 3) { // Unpin the oldest pinned note var oldestPinned = await _context.Notes .Where(n => n.IsPinned && (n.Title == null || !n.Title.StartsWith("#")) && !n.IsDeleted) .OrderBy(n => n.UpdatedAt) .FirstOrDefaultAsync(); if (oldestPinned != null) { oldestPinned.IsPinned = false; oldestPinned.UpdatedAt = now; oldestPinned.LastModifiedBy = userEmail; _logger.LogInformation("Auto-unpinned note {NoteId} (oldest) when pinning note {NewNoteId} by {User}", oldestPinned.Id, noteId, userEmail); } } } // Toggle the pin status note.IsPinned = !note.IsPinned; note.UpdatedAt = now; note.LastModifiedBy = userEmail; await _context.SaveChangesAsync(); _logger.LogInformation("Note {NoteId} {Action} by {User}", noteId, note.IsPinned ? "pinned" : "unpinned", userEmail); } public async Task> GetDeletedNotesAsync() { return await _context.Notes .AsNoTracking() .Where(n => n.IsDeleted) .OrderBy(n => n.Title != null && n.Title.StartsWith("#") ? 1 : 0) // Non-page notes first (0), page notes last (1) .ThenByDescending(n => n.UpdatedAt) // Within each group, order by most recently updated .ToListAsync(); } public async Task RestoreNoteAsync(int noteId) { var note = await _context.Notes .FirstOrDefaultAsync(n => n.Id == noteId); if (note == null) { throw new InvalidOperationException($"Note with ID {noteId} not found."); } if (!note.IsDeleted) { throw new InvalidOperationException($"Note with ID {noteId} is not deleted."); } var userEmail = GetCurrentUserEmail(); var now = DateTime.UtcNow; // Create restore history entry var history = new NoteHistory { NoteId = note.Id, Title = note.Title, Content = note.Content, ModifiedBy = userEmail, ModifiedAt = now, ChangeType = "Restored" }; _context.NoteHistories.Add(history); // Restore the note note.IsDeleted = false; note.UpdatedAt = now; note.LastModifiedBy = userEmail; await _context.SaveChangesAsync(); _logger.LogInformation("Note restored: {NoteId} by {User}", noteId, userEmail); } }