using AutoMapper; using AutoMapper.QueryableExtensions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using OnlyPrompt.Backend.ApiModels.Prompt; using OnlyPrompt.Backend.ApiModels.UserProfile; using OnlyPrompt.Backend.Database; using OnlyPrompt.Backend.Database.Models; using OnlyPrompt.Backend.Utils; using System.ComponentModel.DataAnnotations; namespace OnlyPrompt.Backend.Controllers { [ApiController] [Route("api/v1/prompts")] [Authorize(Roles = ModelConstants.UserRole)] public class PromptController : BaseController { public PromptController(OnlyPromptContext db, IMapper mapper) : base(db, mapper) { } private IQueryable GetAccessiblePrompts(Guid userId) { return _db.Prompts.Where( p => p.SubscriptionTier == null || p.Creator.Subscribers.Any( sub => sub.SubscriberId == userId && p.SubscriptionTier!.Level <= sub.SubscriptionTier!.Level ) ); } [HttpGet] public async Task GetPromptsAsync( [Range(0, int.MaxValue)][FromQuery] int offset = 0, [Range(1, 100)][FromQuery] int limit = 20, [FromQuery] FeedSortType sortBy = FeedSortType.Date, [FromQuery] bool ascending = false, [FromQuery] Identifier? category = null, [FromQuery] string? search = null ) { var userId = User.GetUserId(); var query = _db.Prompts .Where(x => x.CreatorId != userId); if (category.HasValue) query = query.Where(x => category.Value.Id.HasValue ? x.CategoryId == category.Value.Id.Value : x.Category.Slug == category.Value.Slug); if (!string.IsNullOrWhiteSpace(search)) query = query.Where(x => x.Title.Contains(search) || x.Description.Contains(search)); query = sortBy switch { FeedSortType.Date => query.OrderBy(x => x.UpdatedAt, ascending), FeedSortType.Rating => ascending ? query.OrderBy(x => x.Reviews.Average(r => (double?)r.Rating) ?? 0).ThenBy(x => x.Reviews.Count) : query.OrderByDescending(x => x.Reviews.Average(r => (double?)r.Rating) ?? 0).ThenByDescending(x => x.Reviews.Count), _ => query }; var prompts = await query .Skip(offset) .Take(limit) .Select(x => new ApiMinimalPrompt( x.Id, x.Title, x.Description, x.UpdatedAt, x.CreatorId, x.Creator.Profile.DisplayName, x.Creator.Profile.AvatarUrl, x.ExampleImageUrl, x.Price, x.Likes.Count, x.Likes.Any(l => l.UserId == userId), x.Saves.Count, x.Saves.Any(s => s.UserId == userId), x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level, x.SubscriptionTier == null ? null : x.SubscriptionTier.Name, x.SubscriptionTier == null ? (decimal?)null : x.SubscriptionTier.MonthlyPrice, x.Reviews.Average(r => (double?)r.Rating), x.Reviews.Count, x.CreatorId == userId || x.SubscriptionTier == null || x.Creator.Subscribers.Any(s => s.SubscriberId == userId && x.SubscriptionTier.Level <= s.SubscriptionTier.Level) )).ToArrayAsync(); return prompts; } [HttpGet("mine")] public async Task GetOwnPromptsAsync() { var userId = User.GetUserId(); var prompts = await _db.Prompts .Where(x => x.CreatorId == userId) .OrderByDescending(x => x.UpdatedAt) .Select(x => new ApiMinimalPrompt( x.Id, x.Title, x.Description, x.UpdatedAt, x.CreatorId, x.Creator.Profile.DisplayName, x.Creator.Profile.AvatarUrl, x.ExampleImageUrl, x.Price, x.Likes.Count, x.Likes.Any(l => l.UserId == userId), x.Saves.Count, x.Saves.Any(s => s.UserId == userId), x.SubscriptionTier == null ? (int?)null : x.SubscriptionTier.Level, x.SubscriptionTier == null ? null : x.SubscriptionTier.Name, x.SubscriptionTier == null ? (decimal?)null : x.SubscriptionTier.MonthlyPrice, x.Reviews.Average(r => (double?)r.Rating), x.Reviews.Count, true )).ToArrayAsync(); return prompts; } [HttpGet("{id}")] public async Task, NotFound>> GetPromptAsync(Identifier id) { var userId = User.GetUserId(); var prompt = await _db.Prompts .OfIdentifer(id) .FirstOrDefaultAsync(); if (prompt is null) return TypedResults.NotFound("Prompt not found"); var canAccess = await GetAccessiblePrompts(userId.Value).AnyAsync(p => p.Id == prompt.Id); var apiPrompt = _mapper.Map(prompt) with { Content = canAccess ? prompt.Prompt : null, CanAccess = canAccess, IsLiked = prompt.Likes.Any(l => l.UserId == userId), LikeCount = prompt.Likes.Count, IsSaved = prompt.Saves.Any(s => s.UserId == userId), SaveCount = prompt.Saves.Count }; return TypedResults.Ok(apiPrompt); } [HttpPut("{id}")] public async Task, NotFound>> UpdatePromptAsync(Identifier id, [FromBody] ApiUpdatePromptRequest request) { var userId = User.GetUserId(); var prompt = await _db.Prompts .OfIdentifer(id) .FirstOrDefaultAsync(p => p.CreatorId == userId); if (prompt is null) return TypedResults.NotFound("Prompt not found or no permission"); var category = await _db.Categories.FindByIdentifierAsync(new Identifier(request.Category)); if (category is null) return TypedResults.NotFound("Category not found"); prompt.Title = request.Title; prompt.Description = request.Description; prompt.Prompt = request.Content; prompt.Category = category; prompt.ExampleOutput = request.ExampleOutput; prompt.ExampleImageUrl = request.ExampleImageUrl; prompt.Price = null; prompt.SubscriptionTier = null; if (request.SubscriptionTier.HasValue) { var subscriptionTier = await _db.SubscriptionTiers.FirstOrDefaultAsync( t => t.Level == request.SubscriptionTier.Value && t.UserId == userId ); if (subscriptionTier is null) return TypedResults.NotFound("Subscription tier not found"); prompt.SubscriptionTier = subscriptionTier; } await _db.SaveChangesAsync(); var apiPrompt = _mapper.Map(prompt) with { Content = prompt.Prompt, CanAccess = true }; return TypedResults.Ok(apiPrompt); } [HttpPut("{id}/likes")] public async Task, NotFound>> LikePromptAsync(Identifier id) { var userId = User.GetUserId(); var prompt = await _db.Prompts .OfIdentifer(id) .FirstOrDefaultAsync(); if (prompt is null) return TypedResults.NotFound("Prompt not found"); var exists = await _db.PromptLikes.AnyAsync(l => l.PromptId == prompt.Id && l.UserId == userId); if (exists == false) { _db.PromptLikes.Add(new PromptLikeModel { PromptId = prompt.Id, UserId = userId!.Value }); await _db.SaveChangesAsync(); } var count = await _db.PromptLikes.CountAsync(l => l.PromptId == prompt.Id); return TypedResults.Ok(new ApiLikeState(count, true)); } [HttpDelete("{id}/likes")] public async Task, NotFound>> UnlikePromptAsync(Identifier id) { var userId = User.GetUserId(); var prompt = await _db.Prompts .OfIdentifer(id) .FirstOrDefaultAsync(); if (prompt is null) return TypedResults.NotFound("Prompt not found"); await _db.PromptLikes .Where(l => l.PromptId == prompt.Id && l.UserId == userId) .ExecuteDeleteAsync(); var count = await _db.PromptLikes.CountAsync(l => l.PromptId == prompt.Id); return TypedResults.Ok(new ApiLikeState(count, false)); } [HttpPut("{id}/saves")] public async Task, NotFound>> SavePromptAsync(Identifier id) { var userId = User.GetUserId(); var prompt = await _db.Prompts .OfIdentifer(id) .FirstOrDefaultAsync(); if (prompt is null) return TypedResults.NotFound("Prompt not found"); var exists = await _db.PromptSaves.AnyAsync(s => s.PromptId == prompt.Id && s.UserId == userId); if (exists == false) { _db.PromptSaves.Add(new PromptSaveModel { PromptId = prompt.Id, UserId = userId!.Value }); await _db.SaveChangesAsync(); } var count = await _db.PromptSaves.CountAsync(s => s.PromptId == prompt.Id); return TypedResults.Ok(new ApiSaveState(count, true)); } [HttpDelete("{id}/saves")] public async Task, NotFound>> UnsavePromptAsync(Identifier id) { var userId = User.GetUserId(); var prompt = await _db.Prompts .OfIdentifer(id) .FirstOrDefaultAsync(); if (prompt is null) return TypedResults.NotFound("Prompt not found"); await _db.PromptSaves .Where(s => s.PromptId == prompt.Id && s.UserId == userId) .ExecuteDeleteAsync(); var count = await _db.PromptSaves.CountAsync(s => s.PromptId == prompt.Id); return TypedResults.Ok(new ApiSaveState(count, false)); } [HttpDelete("{id}")] public async Task>> DeletePromptAsync(Identifier id) { var userId = User.GetUserId(); var isAdmin = User.IsInRole(ModelConstants.AdminRole); var count = await _db.Prompts .OfIdentifer(id) .Where(p => p.CreatorId == userId || isAdmin) .ExecuteDeleteAsync(); if (count == 0) return TypedResults.NotFound("Prompt not found or no permission"); return TypedResults.NoContent(); } [HttpPost] public async Task, NotFound>> CreatePromptAsync([FromBody] ApiCreatePromptRequest request) { var userId = User.GetUserId(); var category = await _db.Categories.FindByIdentifierAsync(new Identifier(request.Category)); if (category is null) return TypedResults.NotFound("Category not found"); SubscriptionTierModel? subscriptionTier = null; if (request.SubscriptionTier.HasValue) { subscriptionTier = await _db.SubscriptionTiers.FirstOrDefaultAsync( t => t.Level == request.SubscriptionTier.Value && t.UserId == userId ); if (subscriptionTier is null) return TypedResults.NotFound("Subscription tier not found"); } var slug = request.Slug; if (string.IsNullOrEmpty(slug)) slug = await SlugHelper.GenerateUniqueSlugAsync(request.Title, slug => _db.Prompts.AnyAsync(p => p.Slug == slug), ModelConstants.MaxSlugLength); var prompt = new PromptModel { Id = Guid.NewGuid(), Title = request.Title, Description = request.Description, Prompt = request.Content, ExampleOutput = request.ExampleOutput, ExampleImageUrl = request.ExampleImageUrl, Price = null, CreatorId = userId.Value, SubscriptionTier = subscriptionTier, Category = category, Slug = slug }; _db.Prompts.Add(prompt); await _db.SaveChangesAsync(); var apiPrompt = _mapper.Map(prompt); return TypedResults.Ok(apiPrompt); } [HttpGet("{id}/reviews")] public async Task> GetReviewsAsync(Identifier id, [FromQuery] int offset = 0, [Range(1, 200)][FromQuery] int limit = 20) { var userId = User.GetUserId(); var accessiblePrompts = GetAccessiblePrompts(userId!.Value); var reviews = await accessiblePrompts .OfIdentifer(id) .SelectMany(x => x.Reviews) .OrderByDescending(x => x.UpdatedAt) .Skip(offset) .Take(limit) .ProjectTo(_mapper.ConfigurationProvider) .ToArrayAsync(); return TypedResults.Ok(reviews); } [HttpPut("{id}/reviews")] public async Task, BadRequest, NotFound>> AddReviewAsync(Identifier id, [FromBody] ApiCreateReviewRequest request) { var userId = User.GetUserId(); var prompt = await GetAccessiblePrompts(userId!.Value) .OfIdentifer(id) .FirstOrDefaultAsync(); if (prompt is null) return TypedResults.NotFound("Prompt not found or no permission"); if(prompt.CreatorId == userId) return TypedResults.BadRequest("Cannot review your own prompt"); var review = await _db.Reviews.FirstOrDefaultAsync( r => r.PromptId == prompt.Id && r.ReviewerId == userId ); if (review is null) { review = new ReviewModel { PromptId = prompt.Id, ReviewerId = userId.Value, Comment = request.Comment, Rating = request.Rating }; _db.Reviews.Add(review); } else { review.Comment = request.Comment; review.Rating = request.Rating; } await _db.SaveChangesAsync(); var apiReview = _mapper.Map(review); return TypedResults.Ok(apiReview); } [HttpDelete("{promptId}/reviews/{reviewerId}")] public async Task>> DeleteReviewAsync(Identifier promptId, Guid reviewerId) { var userId = User.GetUserId(); var isAdmin = User.IsInRole(ModelConstants.AdminRole); var count = await _db.Reviews .Where( r => (promptId.Id.HasValue ? r.PromptId == promptId.Id : r.Prompt.Slug == promptId.Slug) && (r.ReviewerId == reviewerId || isAdmin) ) .ExecuteDeleteAsync(); if (count == 0) return TypedResults.NotFound("Review not found or no permission"); return TypedResults.NoContent(); } } }