using Extrudex.Domain.Interfaces;
using Extrudex.Infrastructure.Data;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;

namespace Extrudex.Infrastructure.Services;

/// <summary>
/// Calculates the cost of goods sold (COGS) per print job using the spool's
/// purchase price and the print job's derived grams consumed.
///
/// Formula:
///   cost_per_gram = purchase_price / weight_total_grams
///   cost_per_print = grams_derived × cost_per_gram
///
/// Handles missing data gracefully — if the spool has no purchase price or
/// weight recorded, the result includes warnings and null cost fields
/// instead of throwing exceptions.
/// </summary>
public class CostPerPrintService : ICostPerPrintService
{
    private readonly ExtrudexDbContext _dbContext;
    private readonly ILogger<CostPerPrintService> _logger;

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

    /// <inheritdoc />
    public async Task<CostPerPrintResult> CalculateAsync(Guid printJobId, CancellationToken cancellationToken = default)
    {
        _logger.LogDebug("Calculating cost per print for job {PrintJobId}", printJobId);

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

        if (job is null)
        {
            _logger.LogWarning("Print job {PrintJobId} not found for cost calculation", printJobId);
            return new CostPerPrintResult
            {
                PrintJobId = printJobId,
                Warnings = new List<string> { $"Print job with ID '{printJobId}' not found." }
            };
        }

        return BuildResult(job);
    }

    /// <inheritdoc />
    public async Task<IReadOnlyList<CostPerPrintResult>> CalculateBySpoolAsync(
        Guid spoolId, CancellationToken cancellationToken = default)
    {
        _logger.LogDebug("Calculating cost per print for all jobs on spool {SpoolId}", spoolId);

        var jobs = await _dbContext.PrintJobs
            .Include(j => j.Spool)
            .ThenInclude(s => s!.MaterialBase)
            .Where(j => j.SpoolId == spoolId)
            .OrderByDescending(j => j.CreatedAt)
            .ToListAsync(cancellationToken);

        if (jobs.Count == 0)
        {
            _logger.LogDebug("No print jobs found for spool {SpoolId}", spoolId);
            return Array.Empty<CostPerPrintResult>();
        }

        return jobs.Select(BuildResult).ToList();
    }

    /// <summary>
    /// Builds a <see cref="CostPerPrintResult"/> from a print job entity.
    /// Computes cost_per_gram and cost_per_print when all required data is available.
    /// Populates warnings when data is missing or incomplete.
    /// </summary>
    /// <param name="job">The print job entity with Spool navigation loaded.</param>
    /// <returns>A cost calculation result with breakdown and any warnings.</returns>
    private CostPerPrintResult BuildResult(Domain.Entities.PrintJob job)
    {
        var warnings = new List<string>();
        var spool = job.Spool;

        // Map what we always have
        var result = new CostPerPrintResult
        {
            PrintJobId = job.Id,
            PrintName = job.PrintName,
            SpoolId = job.SpoolId,
            SpoolSerial = spool?.SpoolSerial ?? string.Empty,
            MmExtruded = job.MmExtruded,
            GramsDerived = job.GramsDerived,
        };

        // Guard: spool must be loaded
        if (spool is null)
        {
            warnings.Add("Spool data is not available for this print job.");
            result.Warnings = warnings;
            return result;
        }

        // Capture purchase price
        result.PurchasePrice = spool.PurchasePrice;
        result.WeightTotalGrams = spool.WeightTotalGrams;

        // Check for missing purchase price
        if (!spool.PurchasePrice.HasValue)
        {
            warnings.Add(
                "Spool purchase price is not recorded. Cost calculation requires a purchase price on the spool.");
        }

        // Check for zero or negative weight — prevents division by zero
        if (spool.WeightTotalGrams <= 0)
        {
            warnings.Add(
                "Spool total weight is zero or not recorded. Cost calculation requires a positive weight_total_grams on the spool.");
        }

        // Check for zero grams derived
        if (job.GramsDerived <= 0)
        {
            warnings.Add(
                "Derived grams consumed is zero. Ensure mm_extruded, filament diameter, and material density are recorded for this print job.");
        }

        // If all data is present and valid, compute the cost
        if (spool.PurchasePrice.HasValue && spool.WeightTotalGrams > 0 && job.GramsDerived > 0)
        {
            var costPerGram = spool.PurchasePrice.Value / spool.WeightTotalGrams;
            result.CostPerGram = Math.Round(costPerGram, 6);
            result.CostPerPrint = Math.Round(job.GramsDerived * costPerGram, 4);

            _logger.LogDebug(
                "Cost calculated for job {PrintJobId}: {GramsDerived}g × {CostPerGram:C}/g = {CostPerPrint:C}",
                job.Id, job.GramsDerived, result.CostPerGram, result.CostPerPrint);
        }
        else
        {
            _logger.LogDebug(
                "Cost calculation incomplete for job {PrintJobId}: missing data (warnings: {WarningCount})",
                job.Id, warnings.Count);
        }

        result.Warnings = warnings;
        return result;
    }
}