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.AsQueryable(); 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 => query.OrderBy(x => x.Reviews.Average(r => (double?)r.Rating) ?? 2.5, ascending), _ => query }; var prompts = await query .Skip(offset) .Take(limit) .Select(x => new ApiMinimalPrompt( x.Id, x.Title, x.UpdatedAt, x.CreatorId, x.Creator.Profile.DisplayName, x.SubscriptionTier.Level, x.SubscriptionTier.Name, x.Reviews.Average(r => (double?)r.Rating), x.SubscriptionTier == null || x.Creator.Subscribers.Any(s => s.SubscriberId == userId && x.SubscriptionTier.Level < s.SubscriptionTier.Level) )).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 }; return TypedResults.Ok(apiPrompt); } [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(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, 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.Select(x => x.Reviews) .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(); } } }