using Extrudex.API.DTOs.Printers; using Extrudex.Domain.Entities; using Extrudex.Domain.Enums; using Extrudex.Infrastructure.Data; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; namespace Extrudex.API.Controllers; /// /// Controller for managing 3D printers in the fleet. /// Provides CRUD operations, a lightweight status endpoint, and soft-delete. /// Real-time printer status is also available via the SignalR PrinterHub. /// [ApiController] [Route("api/printers")] public class PrintersController : ControllerBase { private readonly ExtrudexDbContext _dbContext; private readonly ILogger _logger; /// /// Initializes a new instance of the class. /// /// The database context for data access. /// The logger for diagnostic output. public PrintersController(ExtrudexDbContext dbContext, ILogger logger) { _dbContext = dbContext; _logger = logger; } /// /// Gets all printers, optionally filtered by active status. /// Results are ordered by printer name alphabetically. /// /// /// Optional filter: true for active printers only, /// false for inactive (soft-deleted) printers, /// omit for all printers. /// /// A list of printers matching the filter criteria. /// Returns the list of printers. [HttpGet] [ProducesResponseType(typeof(IEnumerable), StatusCodes.Status200OK)] public async Task>> GetPrinters( [FromQuery] bool? isActive = null) { _logger.LogDebug("Getting printers, isActive filter: {IsActive}", isActive); var query = _dbContext.Printers.AsQueryable(); if (isActive.HasValue) query = query.Where(p => p.IsActive == isActive.Value); var printers = await query .OrderBy(p => p.Name) .Select(p => MapToPrinterResponse(p)) .ToListAsync(); return Ok(printers); } /// /// Gets a specific printer by its unique identifier. /// /// The unique identifier of the printer. /// The full printer details including connection configuration. /// Returns the printer details. /// The printer was not found. [HttpGet("{id:guid}")] [ProducesResponseType(typeof(PrinterResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetPrinter(Guid id) { _logger.LogDebug("Getting printer {Id}", id); var printer = await _dbContext.Printers.FindAsync(id); if (printer is null) { _logger.LogWarning("Printer {Id} not found", id); return NotFound(new { error = $"Printer with ID '{id}' not found." }); } return Ok(MapToPrinterResponse(printer)); } /// /// Gets the real-time connection/health status of a specific printer. /// This is a lightweight endpoint suitable for polling or dashboard displays. /// For push-based real-time updates, subscribe to the SignalR PrinterHub instead. /// /// The unique identifier of the printer. /// The printer's current status, last-seen timestamp, and active flag. /// Returns the printer status. /// The printer was not found. [HttpGet("{id:guid}/status")] [ProducesResponseType(typeof(PrinterStatusResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task> GetPrinterStatus(Guid id) { _logger.LogDebug("Getting status for printer {Id}", id); var printer = await _dbContext.Printers.FindAsync(id); if (printer is null) { _logger.LogWarning("Printer {Id} not found for status check", id); return NotFound(new { error = $"Printer with ID '{id}' not found." }); } return Ok(new PrinterStatusResponse { Id = printer.Id, Name = printer.Name, Status = printer.Status.ToString(), LastSeenAt = printer.LastSeenAt, IsActive = printer.IsActive }); } /// /// Registers a new printer in the fleet. /// The printer is created with an initial status of Offline. /// The port defaults to 8883 for MQTT or 7125 for Moonraker if not specified. /// /// The printer registration request with connection details. /// The newly created printer. /// The printer was created successfully. /// The request body is invalid or validation failed. [HttpPost] [ProducesResponseType(typeof(PrinterResponse), StatusCodes.Status201Created)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> CreatePrinter( [FromBody] CreatePrinterRequest request) { if (!ModelState.IsValid) return BadRequest(ModelState); _logger.LogInformation("Creating printer: {Name} ({Manufacturer} {Model})", request.Name, request.Manufacturer, request.Model); // Parse enums from string values if (!Enum.TryParse(request.PrinterType, true, out var printerType)) { return BadRequest(new { error = $"Invalid PrinterType: '{request.PrinterType}'. Must be 'Fdm' or 'Resin'." }); } if (!Enum.TryParse(request.ConnectionType, true, out var connectionType)) { return BadRequest(new { error = $"Invalid ConnectionType: '{request.ConnectionType}'. Must be 'Mqtt' or 'Moonraker'." }); } // Default port based on connection type var port = request.Port > 0 ? request.Port : connectionType == ConnectionType.Mqtt ? 8883 : 7125; var entity = new Printer { Name = request.Name, Manufacturer = request.Manufacturer, Model = request.Model, PrinterType = printerType, ConnectionType = connectionType, HostnameOrIp = request.HostnameOrIp, Port = port, MqttUsername = request.MqttUsername, MqttPassword = request.MqttPassword, MqttUseTls = request.MqttUseTls, ApiKey = request.ApiKey, IsActive = request.IsActive, Status = PrinterStatus.Offline }; _dbContext.Printers.Add(entity); await _dbContext.SaveChangesAsync(); var response = MapToPrinterResponse(entity); return CreatedAtAction(nameof(GetPrinter), new { id = entity.Id }, response); } /// /// Updates an existing printer's configuration and connection info. /// Uses full replacement semantics — all fields are written. /// The port defaults to 8883 for MQTT or 7125 for Moonraker if not specified. /// /// The unique identifier of the printer to update. /// The printer update request with new values. /// The updated printer. /// The printer was updated successfully. /// The request body is invalid or validation failed. /// The printer was not found. [HttpPut("{id:guid}")] [ProducesResponseType(typeof(PrinterResponse), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] [ProducesResponseType(StatusCodes.Status400BadRequest)] public async Task> UpdatePrinter( Guid id, [FromBody] UpdatePrinterRequest request) { if (!ModelState.IsValid) return BadRequest(ModelState); _logger.LogInformation("Updating printer {Id}", id); var entity = await _dbContext.Printers.FindAsync(id); if (entity is null) { _logger.LogWarning("Printer {Id} not found for update", id); return NotFound(new { error = $"Printer with ID '{id}' not found." }); } // Parse enums from string values if (!Enum.TryParse(request.PrinterType, true, out var printerType)) { return BadRequest(new { error = $"Invalid PrinterType: '{request.PrinterType}'. Must be 'Fdm' or 'Resin'." }); } if (!Enum.TryParse(request.ConnectionType, true, out var connectionType)) { return BadRequest(new { error = $"Invalid ConnectionType: '{request.ConnectionType}'. Must be 'Mqtt' or 'Moonraker'." }); } var port = request.Port > 0 ? request.Port : connectionType == ConnectionType.Mqtt ? 8883 : 7125; entity.Name = request.Name; entity.Manufacturer = request.Manufacturer; entity.Model = request.Model; entity.PrinterType = printerType; entity.ConnectionType = connectionType; entity.HostnameOrIp = request.HostnameOrIp; entity.Port = port; entity.MqttUsername = request.MqttUsername; entity.MqttPassword = request.MqttPassword; entity.MqttUseTls = request.MqttUseTls; entity.ApiKey = request.ApiKey; entity.IsActive = request.IsActive; await _dbContext.SaveChangesAsync(); return Ok(MapToPrinterResponse(entity)); } /// /// Soft-deletes a printer by setting IsActive to false. /// The printer record is retained for historical and audit purposes. /// Soft-deleted printers can be recovered via PUT with IsActive = true. /// /// The unique identifier of the printer to soft-delete. /// No content on success. /// The printer was soft-deleted successfully. /// The printer was not found. [HttpDelete("{id:guid}")] [ProducesResponseType(StatusCodes.Status204NoContent)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async Task DeletePrinter(Guid id) { _logger.LogInformation("Soft-deleting printer {Id}", id); var entity = await _dbContext.Printers.FindAsync(id); if (entity is null) { _logger.LogWarning("Printer {Id} not found for soft-delete", id); return NotFound(new { error = $"Printer with ID '{id}' not found." }); } entity.IsActive = false; await _dbContext.SaveChangesAsync(); return NoContent(); } // ── Mapping helper ─────────────────────────────────────────────── /// /// Maps a domain entity to a DTO. /// /// The printer entity to map. /// A response DTO suitable for API output. private static PrinterResponse MapToPrinterResponse(Printer p) => new() { Id = p.Id, Status = p.Status.ToString(), Name = p.Name, Manufacturer = p.Manufacturer, Model = p.Model, PrinterType = p.PrinterType.ToString(), ConnectionType = p.ConnectionType.ToString(), HostnameOrIp = p.HostnameOrIp, Port = p.Port, IsActive = p.IsActive, LastSeenAt = p.LastSeenAt, CreatedAt = p.CreatedAt, UpdatedAt = p.UpdatedAt }; }