using Extrudex.API.DTOs;
using Extrudex.API.DTOs.PrintJobs;
using Extrudex.Domain.Entities;
using Extrudex.Domain.Enums;
using Extrudex.Infrastructure.Data;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace Extrudex.API.Controllers;

/// <summary>
/// Controller for managing print jobs — the core operational record of Extrudex.
/// Supports full CRUD with pagination, filtering by printer/spool/status,
/// status transition validation, gram derivation from filament parameters,
/// and soft-delete (sets Status to Cancelled).
/// </summary>
[ApiController]
[Route("api/printjobs")]
public class PrintJobsController : ControllerBase
{
    private readonly ExtrudexDbContext _dbContext;
    private readonly ILogger<PrintJobsController> _logger;

    /// <summary>
    /// Initializes a new instance of the <see cref="PrintJobsController"/> class.
    /// </summary>
    /// <param name="dbContext">The database context for data access.</param>
    /// <param name="logger">The logger for diagnostic output.</param>
    public PrintJobsController(ExtrudexDbContext dbContext, ILogger<PrintJobsController> logger)
    {
        _dbContext = dbContext;
        _logger = logger;
    }

    // ── GET /api/printjobs ────────────────────────────────────────

    /// <summary>
    /// Gets a paginated list of print jobs with optional filtering by
    /// printer, spool, and status. Results are ordered by creation date
    /// (newest first) and include denormalized printer name and spool serial.
    /// </summary>
    /// <param name="query">Optional query parameters for pagination and filtering.</param>
    /// <returns>A paginated list of print jobs matching the filter criteria.</returns>
    /// <response code="200">Returns the paginated list of print jobs.</response>
    [HttpGet]
    [ProducesResponseType(typeof(PagedResponse<PrintJobResponse>), StatusCodes.Status200OK)]
    public async Task<ActionResult<PagedResponse<PrintJobResponse>>> GetPrintJobs(
        [FromQuery] PrintJobQueryParameters query)
    {
        _logger.LogDebug(
            "Getting print jobs: pageNumber={PageNumber}, pageSize={PageSize}, " +
            "printerId={PrinterId}, spoolId={SpoolId}, status={Status}",
            query.PageNumber, query.PageSize, query.PrinterId, query.SpoolId, query.Status);

        // Clamp pagination values
        var pageNumber = Math.Max(1, query.PageNumber);
        var pageSize = Math.Clamp(query.PageSize, 1, 100);

        var jobQuery = _dbContext.PrintJobs
            .Include(j => j.Printer)
            .Include(j => j.Spool)
            .AsQueryable();

        // Apply filters
        if (query.PrinterId.HasValue)
            jobQuery = jobQuery.Where(j => j.PrinterId == query.PrinterId.Value);

        if (query.SpoolId.HasValue)
            jobQuery = jobQuery.Where(j => j.SpoolId == query.SpoolId.Value);

        if (!string.IsNullOrWhiteSpace(query.Status) &&
            Enum.TryParse<JobStatus>(query.Status, ignoreCase: true, out var statusFilter))
        {
            jobQuery = jobQuery.Where(j => j.Status == statusFilter);
        }

        var totalCount = await jobQuery.CountAsync();

        var items = await jobQuery
            .OrderByDescending(j => j.CreatedAt)
            .Skip((pageNumber - 1) * pageSize)
            .Take(pageSize)
            .Select(j => MapToResponse(j))
            .ToListAsync();

        var response = new PagedResponse<PrintJobResponse>
        {
            Items = items,
            TotalCount = totalCount,
            PageNumber = pageNumber,
            PageSize = pageSize
        };

        return Ok(response);
    }

    // ── GET /api/printjobs/{id} ───────────────────────────────────

    /// <summary>
    /// Gets a specific print job by its unique identifier.
    /// Includes denormalized printer name and spool serial for display.
    /// </summary>
    /// <param name="id">The unique identifier of the print job.</param>
    /// <returns>The print job details.</returns>
    /// <response code="200">Returns the print job details.</response>
    /// <response code="404">If the print job with the given ID is not found.</response>
    [HttpGet("{id:guid}")]
    [ProducesResponseType(typeof(PrintJobResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<PrintJobResponse>> GetPrintJob(Guid id)
    {
        _logger.LogDebug("Getting print job {Id}", id);

        var job = await _dbContext.PrintJobs
            .Include(j => j.Printer)
            .Include(j => j.Spool)
            .FirstOrDefaultAsync(j => j.Id == id);

        if (job is null)
        {
            _logger.LogWarning("Print job {Id} not found", id);
            return NotFound(new { error = $"Print job with ID '{id}' not found." });
        }

        return Ok(MapToResponse(job));
    }

    // ── POST /api/printjobs ───────────────────────────────────────

    /// <summary>
    /// Creates a new print job record. Validates that PrinterId and SpoolId
    /// reference existing entities. When AutoDeriveGrams is true, the gram
    /// derivation formula is computed from the spool's material data:
    /// grams = mm_extruded × π × (diameter/2)² × (density/1000).
    /// </summary>
    /// <param name="request">The print job creation request with all required fields.</param>
    /// <returns>The newly created print job with generated ID and timestamps.</returns>
    /// <response code="201">Returns the created print job with location header.</response>
    /// <response code="400">If the request is invalid or foreign keys don't exist.</response>
    [HttpPost]
    [ProducesResponseType(typeof(PrintJobResponse), StatusCodes.Status201Created)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<PrintJobResponse>> CreatePrintJob([FromBody] CreatePrintJobRequest request)
    {
        _logger.LogInformation("Creating print job: {PrintName} on printer {PrinterId} with spool {SpoolId}",
            request.PrintName, request.PrinterId, request.SpoolId);

        // Validate foreign keys exist
        var printer = await _dbContext.Printers.FindAsync(request.PrinterId);
        if (printer is null)
            return BadRequest(new { error = $"Printer with ID '{request.PrinterId}' not found." });

        var spool = await _dbContext.Spools
            .Include(s => s.MaterialBase)
            .FirstOrDefaultAsync(s => s.Id == request.SpoolId);
        if (spool is null)
            return BadRequest(new { error = $"Spool with ID '{request.SpoolId}' not found." });

        // Resolve gram derivation parameters
        decimal filamentDiameter = request.FilamentDiameterAtPrintMm;
        decimal materialDensity = request.MaterialDensityAtPrint;
        decimal gramsDerived = request.GramsDerived;

        if (request.AutoDeriveGrams)
        {
            filamentDiameter = spool.FilamentDiameterMm;
            materialDensity = spool.MaterialBase.DensityGperCm3;
            gramsDerived = DeriveGrams(request.MmExtruded, filamentDiameter, materialDensity);
        }
        else if (request.MmExtruded > 0 && gramsDerived == 0 && materialDensity > 0)
        {
            // Auto-derive if mm_extruded is provided but gramsDerived is zero
            // and we have enough info to compute it
            gramsDerived = DeriveGrams(request.MmExtruded, filamentDiameter, materialDensity);
        }

        // Parse data source enum
        if (!Enum.TryParse<DataSource>(request.DataSource, ignoreCase: true, out var dataSource))
            return BadRequest(new { error = "DataSource must be 'Mqtt', 'Moonraker', or 'Manual'." });

        var entity = new PrintJob
        {
            PrinterId = request.PrinterId,
            SpoolId = request.SpoolId,
            PrintName = request.PrintName,
            GcodeFilePath = request.GcodeFilePath,
            MmExtruded = request.MmExtruded,
            GramsDerived = gramsDerived,
            CostPerPrint = request.CostPerPrint,
            StartedAt = request.StartedAt,
            Status = JobStatus.Queued,
            DataSource = dataSource,
            FilamentDiameterAtPrintMm = filamentDiameter,
            MaterialDensityAtPrint = materialDensity,
            Notes = request.Notes
        };

        _dbContext.PrintJobs.Add(entity);
        await _dbContext.SaveChangesAsync();

        // Reload navigation properties for the response
        await _dbContext.Entry(entity).Reference(j => j.Printer).LoadAsync();
        await _dbContext.Entry(entity).Reference(j => j.Spool).LoadAsync();

        var response = MapToResponse(entity);
        return CreatedAtAction(nameof(GetPrintJob), new { id = entity.Id }, response);
    }

    // ── PUT /api/printjobs/{id} ────────────────────────────────────

    /// <summary>
    /// Updates an existing print job. Validates that PrinterId and SpoolId
    /// reference existing entities. When AutoDeriveGrams is true, gram
    /// derivation is recomputed from the spool's current material data.
    /// </summary>
    /// <param name="id">The unique identifier of the print job to update.</param>
    /// <param name="request">The print job update request with all required fields.</param>
    /// <returns>The updated print job with current timestamps.</returns>
    /// <response code="200">Returns the updated print job.</response>
    /// <response code="404">If the print job with the given ID is not found.</response>
    /// <response code="400">If the request is invalid or foreign keys don't exist.</response>
    [HttpPut("{id:guid}")]
    [ProducesResponseType(typeof(PrintJobResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<PrintJobResponse>> UpdatePrintJob(
        Guid id, [FromBody] UpdatePrintJobRequest request)
    {
        _logger.LogInformation("Updating print job {Id}", id);

        var entity = await _dbContext.PrintJobs.FindAsync(id);
        if (entity is null)
        {
            _logger.LogWarning("Print job {Id} not found for update", id);
            return NotFound(new { error = $"Print job with ID '{id}' not found." });
        }

        // Validate foreign keys exist
        var printer = await _dbContext.Printers.FindAsync(request.PrinterId);
        if (printer is null)
            return BadRequest(new { error = $"Printer with ID '{request.PrinterId}' not found." });

        var spool = await _dbContext.Spools
            .Include(s => s.MaterialBase)
            .FirstOrDefaultAsync(s => s.Id == request.SpoolId);
        if (spool is null)
            return BadRequest(new { error = $"Spool with ID '{request.SpoolId}' not found." });

        // Resolve gram derivation
        decimal gramsDerived = request.GramsDerived;
        decimal filamentDiameter = entity.FilamentDiameterAtPrintMm;
        decimal materialDensity = entity.MaterialDensityAtPrint;

        if (request.AutoDeriveGrams)
        {
            filamentDiameter = spool.FilamentDiameterMm;
            materialDensity = spool.MaterialBase.DensityGperCm3;
            gramsDerived = DeriveGrams(request.MmExtruded, filamentDiameter, materialDensity);
        }

        // Update entity fields
        entity.PrinterId = request.PrinterId;
        entity.SpoolId = request.SpoolId;
        entity.PrintName = request.PrintName;
        entity.GcodeFilePath = request.GcodeFilePath;
        entity.MmExtruded = request.MmExtruded;
        entity.GramsDerived = gramsDerived;
        entity.CostPerPrint = request.CostPerPrint;
        entity.FilamentDiameterAtPrintMm = filamentDiameter;
        entity.MaterialDensityAtPrint = materialDensity;
        entity.Notes = request.Notes;

        // If auto-deriving and the spool changed, also recompute COGS
        if (request.AutoDeriveGrams && spool.PurchasePrice.HasValue && gramsDerived > 0)
        {
            var pricePerGram = spool.PurchasePrice.Value / spool.WeightTotalGrams;
            entity.CostPerPrint = Math.Round(gramsDerived * pricePerGram, 4);
        }

        await _dbContext.SaveChangesAsync();

        // Reload navigation properties
        await _dbContext.Entry(entity).Reference(j => j.Printer).LoadAsync();
        await _dbContext.Entry(entity).Reference(j => j.Spool).LoadAsync();

        return Ok(MapToResponse(entity));
    }

    // ── PATCH /api/printjobs/{id}/status ──────────────────────────

    /// <summary>
    /// Updates the status of a print job with business rule validation.
    /// Allowed transitions:
    /// - Queued → Printing (job starts)
    /// - Printing → Completed (job finishes successfully)
    /// - Any → Cancelled (user cancels)
    /// - Any → Failed (job fails)
    /// - Queued → Queued, Printing → Printing (idempotent)
    /// 
    /// Forbidden transitions:
    /// - Completed → Queued, Printing (cannot reopen a completed job)
    /// - Cancelled → Queued, Printing, Completed (cannot reactivate a cancelled job)
    /// - Failed → Queued, Printing, Completed (cannot reactivate a failed job)
    /// </summary>
    /// <param name="id">The unique identifier of the print job.</param>
    /// <param name="request">The status update request containing the new status.</param>
    /// <returns>The updated print job with the new status.</returns>
    /// <response code="200">Returns the print job with updated status.</response>
    /// <response code="404">If the print job with the given ID is not found.</response>
    /// <response code="400">If the status transition is invalid.</response>
    [HttpPatch("{id:guid}/status")]
    [ProducesResponseType(typeof(PrintJobResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    public async Task<ActionResult<PrintJobResponse>> UpdatePrintJobStatus(
        Guid id, [FromBody] UpdatePrintJobStatusRequest request)
    {
        _logger.LogInformation("Updating print job {Id} status to {NewStatus}", id, request.Status);

        var entity = await _dbContext.PrintJobs.FindAsync(id);
        if (entity is null)
        {
            _logger.LogWarning("Print job {Id} not found for status update", id);
            return NotFound(new { error = $"Print job with ID '{id}' not found." });
        }

        // Parse the target status
        if (!Enum.TryParse<JobStatus>(request.Status, ignoreCase: true, out var newStatus))
        {
            return BadRequest(new { error = "Status must be one of: Queued, Printing, Completed, Cancelled, Failed." });
        }

        // Validate the transition
        var transitionError = ValidateStatusTransition(entity.Status, newStatus);
        if (transitionError is not null)
        {
            _logger.LogWarning(
                "Invalid status transition for print job {Id}: {CurrentStatus} → {NewStatus}. Reason: {Reason}",
                id, entity.Status, newStatus, transitionError);
            return BadRequest(new { error = transitionError });
        }

        // Apply the transition
        var oldStatus = entity.Status;
        entity.Status = newStatus;

        // Set timestamps based on transition
        if (newStatus == JobStatus.Printing && entity.StartedAt is null)
            entity.StartedAt = DateTime.UtcNow;

        if (newStatus is JobStatus.Completed or JobStatus.Failed or JobStatus.Cancelled)
            entity.CompletedAt = DateTime.UtcNow;

        await _dbContext.SaveChangesAsync();

        _logger.LogInformation(
            "Print job {Id} status updated: {OldStatus} → {NewStatus}",
            id, oldStatus, newStatus);

        // Reload navigation properties for the response
        await _dbContext.Entry(entity).Reference(j => j.Printer).LoadAsync();
        await _dbContext.Entry(entity).Reference(j => j.Spool).LoadAsync();

        return Ok(MapToResponse(entity));
    }

    // ── DELETE /api/printjobs/{id} ─────────────────────────────────

    /// <summary>
    /// Soft-deletes a print job by setting its status to Cancelled.
    /// This preserves the job record for historical COGS reporting and audit trails.
    /// If the job is already in a terminal state (Cancelled, Failed, Completed),
    /// the operation is idempotent and returns 204.
    /// </summary>
    /// <param name="id">The unique identifier of the print job to soft-delete.</param>
    /// <returns>No content on success.</returns>
    /// <response code="204">The print job was successfully soft-deleted.</response>
    /// <response code="404">If the print job with the given ID is not found.</response>
    [HttpDelete("{id:guid}")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> DeletePrintJob(Guid id)
    {
        _logger.LogInformation("Soft-deleting print job {Id}", id);

        var entity = await _dbContext.PrintJobs.FindAsync(id);
        if (entity is null)
        {
            _logger.LogWarning("Print job {Id} not found for soft-delete", id);
            return NotFound(new { error = $"Print job with ID '{id}' not found." });
        }

        // If already in a terminal/cancelled state, idempotent no-op
        if (entity.Status == JobStatus.Cancelled)
        {
            _logger.LogDebug("Print job {Id} is already cancelled — idempotent no-op", id);
            return NoContent();
        }

        // Only cancel if it makes sense — don't override Completed status
        if (entity.Status == JobStatus.Completed)
        {
            _logger.LogWarning(
                "Print job {Id} is Completed — cannot soft-delete. Use explicit status change instead.", id);
            return BadRequest(new { error = "Cannot soft-delete a completed print job. Use PATCH /status to change status explicitly." });
        }

        entity.Status = JobStatus.Cancelled;
        entity.CompletedAt = DateTime.UtcNow;
        await _dbContext.SaveChangesAsync();

        _logger.LogInformation("Print job {Id} soft-deleted (Status set to Cancelled)", id);
        return NoContent();
    }

    // ── GET /api/printjobs/{id}/cost-summary ──────────────────────────

    /// <summary>
    /// Gets the material cost summary for a specific print job.
    /// Calculates total material cost from filament usage (grams derived)
    /// and the spool's purchase price. Returns warnings instead of errors
    /// when cost data is unavailable.
    /// </summary>
    /// <param name="id">The unique identifier of the print job.</param>
    /// <returns>A cost summary with breakdown and any warnings about missing data.</returns>
    /// <response code="200">Returns the cost summary. Warnings field lists any missing data.</response>
    /// <response code="404">If the print job with the given ID is not found.</response>
    [HttpGet("{id:guid}/cost-summary")]
    [ProducesResponseType(typeof(CostSummaryResponse), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<ActionResult<CostSummaryResponse>> GetCostSummary(Guid id)
    {
        _logger.LogDebug("Getting cost summary for print job {Id}", id);

        var job = await _dbContext.PrintJobs
            .Include(j => j.Spool)
                .ThenInclude(s => s!.MaterialBase)
            .FirstOrDefaultAsync(j => j.Id == id);

        if (job is null)
        {
            _logger.LogWarning("Print job {Id} not found for cost summary", id);
            return NotFound(new { error = $"Print job with ID '{id}' not found." });
        }

        var warnings = new List<string>();
        var spool = job.Spool;

        // Build response with what we have
        var response = new CostSummaryResponse
        {
            PrintJobId = job.Id,
            PrintName = job.PrintName,
            SpoolId = job.SpoolId,
            SpoolSerial = spool?.SpoolSerial ?? string.Empty,
            SpoolBrand = spool?.Brand ?? string.Empty,
            SpoolColorName = spool?.ColorName ?? string.Empty,
            MmExtruded = job.MmExtruded,
            GramsDerived = job.GramsDerived,
            SpoolPurchasePrice = spool?.PurchasePrice,
            SpoolWeightTotalGrams = spool?.WeightTotalGrams,
            StoredCostPerPrint = job.CostPerPrint
        };

        // Validate spool data availability
        if (spool is null)
        {
            warnings.Add("Spool data is not available for this print job. Cost cannot be calculated.");
            response.Warnings = warnings;
            return Ok(response);
        }

        // Check if we can calculate cost
        if (!spool.PurchasePrice.HasValue)
        {
            warnings.Add("Spool purchase price is not set. Cost per gram and total material cost cannot be calculated.");
        }

        if (spool.WeightTotalGrams <= 0)
        {
            warnings.Add("Spool total weight is zero or invalid. Cost per gram and total material cost cannot be calculated.");
        }

        // If we have enough data, calculate the cost
        if (spool.PurchasePrice.HasValue && spool.WeightTotalGrams > 0)
        {
            var pricePerGram = spool.PurchasePrice.Value / spool.WeightTotalGrams;
            response.PricePerGram = Math.Round(pricePerGram, 4);
            response.TotalMaterialCost = Math.Round(job.GramsDerived * pricePerGram, 4);
        }

        // Warn if grams derived is zero but mm extruded is non-zero
        if (job.GramsDerived == 0 && job.MmExtruded > 0)
        {
            warnings.Add("GramsDerived is zero despite MmExtruded being non-zero. Cost may be inaccurate. Consider re-deriving grams from filament parameters.");
        }

        response.Warnings = warnings;
        return Ok(response);
    }

    // ── Gram Derivation Formula ────────────────────────────────────

    /// <summary>
    /// Derives grams consumed from millimeters extruded, filament diameter, and material density.
    /// Formula: grams = mm_extruded × cross_section_area_mm² × (density_g_per_cm³ / 1000)
    /// where cross_section_area = π × (diameter_mm / 2)²
    /// The density division by 1000 converts g/cm³ to g/mm³.
    /// </summary>
    /// <param name="mmExtruded">Total millimeters of filament extruded.</param>
    /// <param name="filamentDiameterMm">Filament diameter in mm (typically 1.75mm).</param>
    /// <param name="densityGPerCm3">Material density in g/cm³ (e.g., 1.24 for PLA).</param>
    /// <returns>Derived grams consumed, rounded to 2 decimal places.</returns>
    private static decimal DeriveGrams(decimal mmExtruded, decimal filamentDiameterMm, decimal densityGPerCm3)
    {
        // Cross-section area in mm²: π × r² where r = diameter/2
        var radiusMm = filamentDiameterMm / 2m;
        var crossSectionAreaMm2 = Math.Round(Math.PI * (double)(radiusMm * radiusMm), 8);

        // Convert density from g/cm³ to g/mm³ by dividing by 1000
        var densityGMm3 = (double)densityGPerCm3 / 1000.0;

        // grams = mm × area × density
        var grams = (double)mmExtruded * crossSectionAreaMm2 * densityGMm3;

        return Math.Round((decimal)grams, 2);
    }

    // ── Status Transition Validation ───────────────────────────────

    /// <summary>
    /// Validates that a status transition is allowed by business rules.
    /// Returns null if the transition is valid, or an error message if not.
    /// </summary>
    /// <param name="currentStatus">The current status of the print job.</param>
    /// <param name="newStatus">The desired new status.</param>
    /// <returns>null if valid; an error message string if invalid.</returns>
    private static string? ValidateStatusTransition(JobStatus currentStatus, JobStatus newStatus)
    {
        // Idempotent transitions are always allowed
        if (currentStatus == newStatus)
            return null;

        // Any state can transition to Cancelled or Failed
        if (newStatus is JobStatus.Cancelled or JobStatus.Failed)
            return null;

        // Queued can transition to Printing
        if (currentStatus == JobStatus.Queued && newStatus == JobStatus.Printing)
            return null;

        // Printing can transition to Completed
        if (currentStatus == JobStatus.Printing && newStatus == JobStatus.Completed)
            return null;

        // All other transitions are forbidden
        return currentStatus switch
        {
            JobStatus.Completed =>
                $"Cannot change status from '{currentStatus}' to '{newStatus}'. Completed jobs cannot be reactivated.",
            JobStatus.Cancelled =>
                $"Cannot change status from '{currentStatus}' to '{newStatus}'. Cancelled jobs cannot be reactivated.",
            JobStatus.Failed =>
                $"Cannot change status from '{currentStatus}' to '{newStatus}'. Failed jobs cannot be reactivated.",
            JobStatus.Printing =>
                $"Cannot change status from '{currentStatus}' to '{newStatus}'. A printing job can only transition to Completed, Cancelled, or Failed.",
            JobStatus.Queued =>
                $"Cannot change status from '{currentStatus}' to '{newStatus}'. A queued job can only transition to Printing, Cancelled, or Failed.",
            _ =>
                $"Invalid status transition from '{currentStatus}' to '{newStatus}'."
        };
    }

    // ── Mapping Helper ─────────────────────────────────────────────

    private static PrintJobResponse MapToResponse(PrintJob j) => new()
    {
        Id = j.Id,
        PrinterId = j.PrinterId,
        PrinterName = j.Printer?.Name ?? string.Empty,
        SpoolId = j.SpoolId,
        SpoolSerial = j.Spool?.SpoolSerial ?? string.Empty,
        PrintName = j.PrintName,
        GcodeFilePath = j.GcodeFilePath,
        MmExtruded = j.MmExtruded,
        GramsDerived = j.GramsDerived,
        CostPerPrint = j.CostPerPrint,
        StartedAt = j.StartedAt,
        CompletedAt = j.CompletedAt,
        Status = j.Status.ToString(),
        DataSource = j.DataSource.ToString(),
        FilamentDiameterAtPrintMm = j.FilamentDiameterAtPrintMm,
        MaterialDensityAtPrint = j.MaterialDensityAtPrint,
        Notes = j.Notes,
        CreatedAt = j.CreatedAt,
        UpdatedAt = j.UpdatedAt
    };
}