using Microsoft.Extensions.Logging;
using PowderCoating.Application.DTOs.Quote;
using PowderCoating.Application.Interfaces;
using PowderCoating.Core.Entities;
using PowderCoating.Core.Interfaces;

namespace PowderCoating.Application.Services;

/// <summary>
/// Core pricing engine for quotes and jobs. All dollar amounts produced here are stored as
/// snapshots on the Quote/Job rows at save time — they are NOT recalculated on every page load.
/// This means a quote's displayed total is always exactly what was presented to the customer,
/// even if operating costs change after the quote was created.
///
/// Pricing flow (high-level):
///   1. Per-coat: material cost + labor cost  →  <see cref="CalculateCoatPriceAsync"/>
///   2. Per-item: base + additional coats + complexity  →  <see cref="CalculateQuoteItemPriceAsync"/>
///   3. Quote total: items + oven batch + shop supplies + discounts + rush + tax  →  <see cref="CalculateQuoteTotalsAsync"/>
///
/// Key design decisions documented inline:
///   - Custom powder charges the full ordered quantity; in-stock charges calculated usage only.
///   - Markup is baked into item prices, NOT added as a separate line at the quote level.
///   - Overhead was removed as a separate charge — it is now absorbed into the markup %.
///   - AI items bypass the pricing engine entirely (ManualUnitPrice used as-is).
///   - Oven cost is a quote-level batch charge, not per-item; scaled for AI item fraction.
/// </summary>
public class PricingCalculationService : IPricingCalculationService
{
    private readonly IUnitOfWork _unitOfWork;
    private readonly ILogger<PricingCalculationService> _logger;
    private readonly IMeasurementConversionService _measurementService;
    private readonly ITenantContext _tenantContext;

    // Constants for additional cost calculations
    private const decimal ConsumablesSurchargePercent = 0.05m; // 5% of material costs

    // Sandblasting and masking multipliers removed - labor is the same for all tasks
    // private const decimal SandblastingLaborMultiplier = 1.5m; // 1.5x base labor
    // private const decimal MaskingLaborMultiplier = 0.5m; // 0.5x base labor
    // private const decimal SandblastingTimePercent = 0.3m; // 30% of estimated time

    public PricingCalculationService(
        IUnitOfWork unitOfWork,
        ILogger<PricingCalculationService> logger,
        IMeasurementConversionService measurementService,
        ITenantContext tenantContext)
    {
        _unitOfWork = unitOfWork;
        _logger = logger;
        _measurementService = measurementService;
        _tenantContext = tenantContext;
    }

    /// <summary>
    /// Loads the operating costs row for a company. Returns null (and logs a warning) if none
    /// is configured — callers fall back to zero pricing rather than throwing, so a misconfigured
    /// company shows $0 quotes rather than crashing the wizard.
    /// </summary>
    public async Task<CompanyOperatingCosts?> GetOperatingCostsAsync(int companyId)
    {
        try
        {
            var costs = await _unitOfWork.CompanyOperatingCosts.FindAsync(c => c.CompanyId == companyId);
            return costs.FirstOrDefault();
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Error retrieving operating costs for company {CompanyId}", companyId);
            return null;
        }
    }

    /// <summary>
    /// Calculates the material and labor cost for a single coat on a single quote item.
    ///
    /// Material cost rules:
    ///   - Custom powder (no InventoryItemId, manual PowderCostPerLb): charges for the full
    ///     PowderToOrder quantity, not just calculated usage. The shop must purchase the whole
    ///     bag/order for this job, so the customer pays for all of it.
    ///   - In-stock powder (InventoryItemId set): charges calculated usage only
    ///     (surface area × lbs/sqft ÷ transfer efficiency × unit cost).
    ///   - No surface area, no PowderToOrder → $0 material cost.
    ///
    /// Labor cost rules:
    ///   - First coat (coatIndex 0): 100% of EstimatedMinutes × StandardLaborRate.
    ///   - Additional coats: AdditionalCoatLaborPercent % of base minutes.
    ///   - NoExtraLayerCharge flag → 0% multiplier for that coat (used for clear coats, etc.).
    ///
    /// Surface area is converted from m² to sqft when the tenant uses metric, so all internal
    /// math stays in imperial regardless of the UI unit setting.
    /// </summary>
    public async Task<QuoteItemCoatPricingResult> CalculateCoatPriceAsync(
        CreateQuoteItemCoatDto coat,
        decimal itemSurfaceAreaSqFt,
        decimal quantity,
        int coatIndex,
        int estimatedMinutesBase,
        int companyId)
    {
        var costs = await GetOperatingCostsAsync(companyId);
        if (costs == null)
        {
            _logger.LogWarning("No operating costs configured for company {CompanyId}, using default values", companyId);
            return new QuoteItemCoatPricingResult
            {
                CoatMaterialCost = 0,
                CoatLaborCost = 0,
                CoatTotalCost = 0
            };
        }

        // Convert from metric if needed
        var useMetric = await _tenantContext.UseMetricSystemAsync();
        var perItemSurfaceAreaSqFt = itemSurfaceAreaSqFt;

        if (useMetric)
        {
            perItemSurfaceAreaSqFt = _measurementService.SquareMetersToFeet(itemSurfaceAreaSqFt);
            _logger.LogInformation("Converted surface area from {SqM} sq m to {SqFt} sq ft",
                itemSurfaceAreaSqFt, perItemSurfaceAreaSqFt);
        }

        // 1. Calculate powder cost per sq ft for this coat (also track raw costPerLb for order-quantity billing)
        decimal powderCostPerSqFt = costs.PowderCoatingCostPerSqFt; // Default
        decimal costPerLb = 0m;

        // A coat is "custom" (must be purchased) when it has no inventory item but has a manual price.
        // In-stock coats reference an inventory item that already has stock on hand.
        // Incoming coats reference an inventory item with IsIncoming=true (ordered, not yet received).
        bool isCustomPowder = !coat.InventoryItemId.HasValue
                              && coat.PowderCostPerLb.HasValue
                              && coat.PowderCostPerLb.Value > 0;
        bool isIncomingPowder = false;

        if (coat.PowderCostPerLb.HasValue && coat.PowderCostPerLb.Value > 0)
        {
            // Custom powder with manual cost
            costPerLb = coat.PowderCostPerLb.Value;
            var poundsPerSqFt = 1m / coat.CoverageSqFtPerLb;
            var actualPoundsPerSqFt = poundsPerSqFt / (coat.TransferEfficiency / 100m);
            powderCostPerSqFt = actualPoundsPerSqFt * costPerLb;

            _logger.LogInformation("Coat {CoatName}: Using custom powder cost: {Cost}/sqft",
                coat.CoatName, powderCostPerSqFt);
        }
        else if (coat.InventoryItemId.HasValue && coat.InventoryItemId.Value > 0)
        {
            // In-stock or incoming powder - use inventory cost
            try
            {
                var inventoryItem = await _unitOfWork.InventoryItems.GetByIdAsync(coat.InventoryItemId.Value);
                // Prefer the current catalog price (replacement cost) so quotes reflect the latest
                // price; fall back to the item's own cost when it isn't catalog-linked.
                var effectiveCostPerLb = inventoryItem?.CatalogReferencePrice ?? inventoryItem?.UnitCost ?? 0m;
                if (inventoryItem != null && effectiveCostPerLb > 0)
                {
                    costPerLb = effectiveCostPerLb;
                    isIncomingPowder = inventoryItem.IsIncoming;
                    var coverage = coat.CoverageSqFtPerLb;
                    var transferEfficiency = coat.TransferEfficiency;

                    var poundsPerSqFt = 1m / coverage;
                    var actualPoundsPerSqFt = poundsPerSqFt / (transferEfficiency / 100m);
                    powderCostPerSqFt = actualPoundsPerSqFt * costPerLb;

                    _logger.LogInformation("Coat {CoatName}: Using inventory item: {InventoryItem} (IsIncoming={IsIncoming}), CostPerLb={CostPerLb}/lb (catalog ref={CatalogRef}), Coverage={Coverage}sqft/lb, Efficiency={Efficiency}%, Calculated={CalcCost}/sqft",
                        coat.CoatName, inventoryItem.Name, isIncomingPowder, costPerLb, inventoryItem.CatalogReferencePrice, coverage, transferEfficiency, powderCostPerSqFt);
                }
            }
            catch (Exception ex)
            {
                _logger.LogWarning(ex, "Could not retrieve inventory item {InventoryItemId} for coat {CoatName}, using default powder cost",
                    coat.InventoryItemId.Value, coat.CoatName);
            }
        }

        // 2. Calculate material cost for this coat
        var batchSurfaceAreaSqFt = perItemSurfaceAreaSqFt * quantity;
        decimal coatMaterialCost;

        // Custom or incoming powder must be purchased for this job — charge for the full ordered
        // quantity so the shop recovers the actual outlay, not just the calculated usage.
        if (batchSurfaceAreaSqFt > 0 && (isCustomPowder || isIncomingPowder) && coat.PowderToOrder.HasValue && coat.PowderToOrder.Value > 0)
        {
            coatMaterialCost = coat.PowderToOrder.Value * costPerLb;
            _logger.LogInformation("Coat {CoatName}: {PowderKind} powder to order — charging full order qty {Lbs}lb × ${CostPerLb}/lb = ${Total} (calculated usage would have been ${Calc})",
                coat.CoatName, isIncomingPowder ? "Incoming" : "Custom", coat.PowderToOrder.Value, costPerLb, coatMaterialCost, batchSurfaceAreaSqFt * powderCostPerSqFt);
        }
        else if (batchSurfaceAreaSqFt > 0)
        {
            // In-stock powder: charge for calculated usage only
            coatMaterialCost = batchSurfaceAreaSqFt * powderCostPerSqFt;
        }
        else if (coat.PowderToOrder.HasValue && coat.PowderToOrder.Value > 0)
        {
            // No surface area (generic item): use PowderToOrder × cost per lb directly
            coatMaterialCost = coat.PowderToOrder.Value * costPerLb;
            _logger.LogInformation("Coat {CoatName}: Using PowderToOrder={Lbs}lb × ${CostPerLb}/lb = ${Total}",
                coat.CoatName, coat.PowderToOrder.Value, costPerLb, coatMaterialCost);
        }
        else
        {
            coatMaterialCost = 0;
        }

        // 3. Calculate labor cost for this coat
        // First coat = 100% of base minutes, each additional coat = configurable percent from operating costs
        // NoExtraLayerCharge flag skips the additional layer charge (0x) for that specific coat
        var additionalCoatPercent = costs.AdditionalCoatLaborPercent / 100m;
        var laborMultiplier = coatIndex == 0 ? 1.0m : (coat.NoExtraLayerCharge ? 0m : additionalCoatPercent);
        var coatMinutes = estimatedMinutesBase * laborMultiplier * quantity;
        var coatLaborHours = coatMinutes / 60m;
        var coatLaborCost = coatLaborHours * costs.StandardLaborRate;

        _logger.LogInformation("Coat {CoatName} (index {Index}): Labor={LaborMult}x, Minutes={Minutes}, LaborCost=${LaborCost}",
            coat.CoatName, coatIndex, laborMultiplier, coatMinutes, coatLaborCost);

        return new QuoteItemCoatPricingResult
        {
            CoatMaterialCost = coatMaterialCost,
            CoatLaborCost = coatLaborCost,
            CoatTotalCost = coatMaterialCost + coatLaborCost
        };
    }

    /// <summary>
    /// Returns true when a coat requires ordering custom powder that is not in inventory.
    /// Only coats with an explicit PowderToOrder quantity qualify — coats without a quantity
    /// fall through to the standard surface-area pricing path in CalculateCoatPriceAsync.
    /// </summary>
    private static bool IsCustomPowderCoat(CreateQuoteItemCoatDto coat) =>
        !coat.InventoryItemId.HasValue &&
        coat.PowderToOrder.HasValue && coat.PowderToOrder.Value > 0 &&
        coat.PowderCostPerLb.HasValue && coat.PowderCostPerLb.Value > 0;

    /// <summary>
    /// Calculates the total price for a single quote line item, routing to the correct pricing
    /// path based on item type:
    ///
    ///   • AI item (IsAiItem = true)     — ManualUnitPrice used directly; engine skipped entirely.
    ///   • Sales item (IsSalesItem)       — ManualUnitPrice × Qty; no coating math.
    ///   • Generic item (IsGenericItem)   — ManualUnitPrice as base; custom powder coat material added on top.
    ///   • Labor item (IsLaborItem)       — Qty treated as hours × StandardLaborRate; no markup.
    ///   • Catalog, no coats             — DefaultPrice × Qty (or PowderCostOverride if set).
    ///   • Catalog with coats            — DefaultPrice base + custom powder coat costs on top;
    ///                                     in-stock powder assumed baked into the catalog price.
    ///   • Calculated (surface area)      — Material + labor + consumables surcharge + coating booth
    ///                                     + markup + additional coat % + complexity surcharge.
    ///
    /// Markup (GeneralMarkupPercentage) is applied to materials only for calculated items —
    /// labor and equipment rates are already the billable customer rates, not cost rates.
    /// Prep service labor is added to the base for non-AI items when IncludePrepCost is true.
    /// </summary>
    public async Task<QuoteItemPricingResult> CalculateQuoteItemPriceAsync(CreateQuoteItemDto item, int companyId, decimal? ovenCostOverride = null)
    {
        var costs = await GetOperatingCostsAsync(companyId);
        if (costs == null)
        {
            _logger.LogWarning("No operating costs configured for company {CompanyId}, using default values", companyId);
            return new QuoteItemPricingResult
            {
                MaterialCost = 0,
                LaborCost = 0,
                EquipmentCost = 0,
                ItemSubtotal = 0,
                UnitPrice = 0,
                TotalPrice = 0
            };
        }

        // AI items use ManualUnitPrice as the single-coat base price.
        // Apply the same additional-coat charge as the calculated-item path so that
        // adding a 2nd or 3rd coat in step 3 increases the price by AdditionalCoatLaborPercent%
        // per coat — matching what catalog/calculated items charge.
        if (item.IsAiItem && item.ManualUnitPrice.HasValue)
        {
            var aiUnitPrice = item.ManualUnitPrice.Value;

            int additionalAiCoats = 0;
            if (item.Coats != null)
            {
                for (int i = 1; i < item.Coats.Count; i++)
                {
                    if (!item.Coats[i].NoExtraLayerCharge)
                        additionalAiCoats++;
                }
            }
            if (additionalAiCoats > 0)
                aiUnitPrice = Math.Round(
                    aiUnitPrice * (1m + additionalAiCoats * (costs.AdditionalCoatLaborPercent / 100m)), 2);

            var aiTotal = aiUnitPrice * item.Quantity;
            return new QuoteItemPricingResult
            {
                MaterialCost = 0,
                LaborCost = 0,
                EquipmentCost = 0,
                ItemSubtotal = aiTotal,
                UnitPrice = aiUnitPrice,
                TotalPrice = aiTotal
            };
        }

        // Custom formula items (FixedRate mode): the wizard evaluated the NCalc formula server-side
        // and stored the per-item result as ManualUnitPrice. Multiply by Quantity for the total,
        // exactly like every other item type that uses ManualUnitPrice.
        // SurfaceAreaSqFt mode: ManualUnitPrice is null; the formula produced sqft which was stored
        // in SurfaceAreaSqFt, so the item falls through to the standard calculated path below.
        if (item.IsCustomFormulaItem && item.ManualUnitPrice.HasValue)
        {
            var formulaUnitPrice = item.ManualUnitPrice.Value;
            var formulaTotal = formulaUnitPrice * item.Quantity;
            return new QuoteItemPricingResult
            {
                MaterialCost  = 0,
                LaborCost     = 0,
                EquipmentCost = 0,
                ItemSubtotal  = formulaTotal,
                UnitPrice     = formulaUnitPrice,
                TotalPrice    = formulaTotal
            };
        }

        // Sales items (off-the-shelf merchandise) — manual unit price, no coating calculation.
        if (item.IsSalesItem && item.ManualUnitPrice.HasValue)
        {
            var total = item.ManualUnitPrice.Value * item.Quantity;
            return new QuoteItemPricingResult
            {
                MaterialCost  = 0,
                LaborCost     = 0,
                EquipmentCost = 0,
                ItemSubtotal  = total,
                UnitPrice     = item.ManualUnitPrice.Value,
                TotalPrice    = total
            };
        }

        // Generic items use a manually-entered flat price as the base;
        // coat material costs (powder) are calculated and added on top.
        if (item.IsGenericItem && item.ManualUnitPrice.HasValue)
        {
            decimal coatMaterialCost = 0;
            if (item.Coats != null && item.Coats.Any())
            {
                for (int i = 0; i < item.Coats.Count; i++)
                {
                    // Custom powder material moves to the "Custom Powder Order" line item
                    if (IsCustomPowderCoat(item.Coats[i])) continue;
                    var coatResult = await CalculateCoatPriceAsync(
                        item.Coats[i], 0m, item.Quantity, i, 0, companyId);
                    coatMaterialCost += coatResult.CoatMaterialCost;
                }
            }
            var baseTotal = item.ManualUnitPrice.Value * item.Quantity;
            var grandTotal = baseTotal + coatMaterialCost;
            return new QuoteItemPricingResult
            {
                MaterialCost = coatMaterialCost,
                LaborCost = 0,
                EquipmentCost = 0,
                ItemSubtotal = grandTotal,
                UnitPrice = grandTotal / item.Quantity,
                TotalPrice = grandTotal
            };
        }

        // Labor items: Quantity = hours, priced at StandardLaborRate.
        // No markup applied — the labor rate is already the billable customer rate.
        if (item.IsLaborItem)
        {
            var laborHours = (decimal)item.Quantity;
            var laborTotal = laborHours * costs.StandardLaborRate;
            _logger.LogInformation("Labor item '{Description}': {Hours}h × ${Rate}/h = ${Total}",
                item.Description, laborHours, costs.StandardLaborRate, laborTotal);
            return new QuoteItemPricingResult
            {
                MaterialCost = 0,
                LaborCost = laborTotal,
                EquipmentCost = 0,
                ItemSubtotal = laborTotal,
                UnitPrice = costs.StandardLaborRate,
                TotalPrice = laborTotal
            };
        }

        // If catalog item with no coats, use override price or catalog default price
        if (item.CatalogItemId.HasValue && (item.Coats == null || !item.Coats.Any()))
        {
            decimal noCoatUnitPrice = item.PowderCostOverride ?? 0m;
            if (noCoatUnitPrice == 0m)
            {
                var catalogItem = await _unitOfWork.CatalogItems.GetByIdAsync(item.CatalogItemId.Value);
                noCoatUnitPrice = catalogItem?.DefaultPrice ?? 0m;
            }
            var catalogTotal = noCoatUnitPrice * item.Quantity;
            return new QuoteItemPricingResult
            {
                MaterialCost = 0,
                LaborCost = 0,
                EquipmentCost = 0,
                ItemSubtotal = catalogTotal,
                UnitPrice = noCoatUnitPrice,
                TotalPrice = catalogTotal
            };
        }

        // ── Step 1: calculate base price ──────────────────────────────────────────
        // For catalog items: DefaultPrice × Qty is the base (covers one standard coat).
        // For non-catalog items: calculate first coat's material + labor + markup as the base.
        bool isCatalogItem = item.CatalogItemId.HasValue;

        _logger.LogInformation("Item {Description}: Qty={Qty}, SqFt={SqFt}, Coats={CoatCount}, IsCatalog={IsCatalog}",
            item.Description, item.Quantity, item.SurfaceAreaSqFt, item.Coats?.Count ?? 0, isCatalogItem);

        decimal baseSubtotal;
        decimal totalMaterialCost = 0m;
        decimal totalLaborCost = 0m;
        decimal totalEquipmentCost = 0m;

        if (isCatalogItem)
        {
            decimal baseUnitPrice = item.PowderCostOverride ?? 0m;
            if (baseUnitPrice == 0m)
            {
                var catalogItem = await _unitOfWork.CatalogItems.GetByIdAsync(item.CatalogItemId!.Value);
                baseUnitPrice = catalogItem?.DefaultPrice ?? 0m;
            }
            baseSubtotal = baseUnitPrice * item.Quantity;
            _logger.LogInformation("Catalog base: ${Price} × {Qty} = ${Base}", baseUnitPrice, item.Quantity, baseSubtotal);

            // Optionally add prep service labor for catalog items when the job requires abnormal
            // prep time (e.g. 120-min outgassing on a part that normally takes 30). The flag
            // defaults to false so the DefaultPrice is used as-is for typical jobs.
            if (!item.IsAiItem && item.IncludePrepCost && item.PrepServices != null && item.PrepServices.Any())
            {
                var totalPrepMinutes = item.PrepServices.Sum(ps => ps.EstimatedMinutes);
                var prepLaborCost = (totalPrepMinutes / 60m) * costs.StandardLaborRate;
                baseSubtotal += prepLaborCost;
                totalLaborCost += prepLaborCost;
                _logger.LogInformation("Catalog item extra prep: {Min} min × ${Rate}/hr = ${Cost}", totalPrepMinutes, costs.StandardLaborRate, prepLaborCost);
            }

            // Custom (non-inventory) powder coats must be purchased separately — add their material
            // cost on top of the catalog base price. Inventory powder is assumed baked into DefaultPrice.
            if (item.Coats != null && item.Coats.Any())
            {
                for (int ci = 0; ci < item.Coats.Count; ci++)
                {
                    var coat = item.Coats[ci];
                    // Custom powder with PowderToOrder moves to the "Custom Powder Order" line item; skip here
                    if (!coat.InventoryItemId.HasValue && coat.PowderCostPerLb.HasValue && coat.PowderCostPerLb.Value > 0
                        && !IsCustomPowderCoat(coat))
                    {
                        var coatResult = await CalculateCoatPriceAsync(coat, item.SurfaceAreaSqFt, item.Quantity, ci, 0, companyId);
                        totalMaterialCost += coatResult.CoatMaterialCost;
                        baseSubtotal += coatResult.CoatMaterialCost;
                        _logger.LogInformation("Catalog item custom powder coat {Name}: +${Cost}", coat.CoatName, coatResult.CoatMaterialCost);
                    }
                }
            }
        }
        else
        {
            // Non-catalog: derive base from first coat's material + labor + equipment + markup
            decimal coatLaborCost = 0m; // coat-only labor, used for coating booth (not prep/sandblast)
            if (item.Coats != null && item.Coats.Count > 0)
            {
                var firstCoatResult = await CalculateCoatPriceAsync(
                    item.Coats[0], item.SurfaceAreaSqFt, item.Quantity, 0, item.EstimatedMinutes, companyId);
                // Custom powder material moves to the "Custom Powder Order" line item; keep the labor
                totalMaterialCost = IsCustomPowderCoat(item.Coats[0]) ? 0m : firstCoatResult.CoatMaterialCost;
                coatLaborCost     = firstCoatResult.CoatLaborCost;
                totalLaborCost    = coatLaborCost;
            }

            // Prep service labor (done once per item batch)
            // AI items exclude prep cost — it's already baked into the AI estimate
            if (!item.IsAiItem && item.PrepServices != null && item.PrepServices.Any())
            {
                var totalPrepMinutes = item.PrepServices.Sum(ps => ps.EstimatedMinutes);
                var prepLaborHours = totalPrepMinutes / 60m;
                totalLaborCost += prepLaborHours * costs.StandardLaborRate;
                _logger.LogInformation("PrepServices={Count}, TotalPrepMinutes={Min}", item.PrepServices.Count, totalPrepMinutes);
            }

            // Consumables surcharge (5% of material)
            totalMaterialCost += totalMaterialCost * ConsumablesSurchargePercent;

            // Equipment cost: coating booth only — use coat labor hours, not prep/sandblast hours
            // (sandblasting happens in a blast cabinet, not the powder coating booth)
            var coatLaborHours = costs.StandardLaborRate > 0 ? coatLaborCost / costs.StandardLaborRate : 0m;
            totalEquipmentCost = coatLaborHours * costs.CoatingBoothCostPerHour;

            // Apply pricing mode: markup on material only, or target margin on total cost
            if (costs.PricingMode == PowderCoating.Core.Enums.PricingMode.MarginOnTotalCost)
            {
                var totalCost = totalMaterialCost + totalLaborCost + totalEquipmentCost;
                var margin = Math.Min(costs.TargetMarginPercent / 100m, 0.99m); // clamp to avoid divide-by-zero
                baseSubtotal = margin < 0.001m ? totalCost : totalCost / (1m - margin);
                _logger.LogInformation("Non-catalog base (margin mode): TotalCost=${C}, Margin={M}% → Base=${Base}",
                    totalCost, costs.TargetMarginPercent, baseSubtotal);
            }
            else
            {
                var materialWithMarkup = totalMaterialCost * (1 + costs.GeneralMarkupPercentage / 100m);
                baseSubtotal = materialWithMarkup + totalLaborCost + totalEquipmentCost;
                _logger.LogInformation("Non-catalog base (markup mode): Material=${M} (markup {Mk}% → ${MW}), Labor=${L}, Equipment=${E}, Base=${Base}",
                    totalMaterialCost, costs.GeneralMarkupPercentage, materialWithMarkup, totalLaborCost, totalEquipmentCost, baseSubtotal);
            }
        }

        // ── Step 2: additional coat charges (% of base per additional coat) ──────
        // Each additional coat (index 1+) without NoExtraLayerCharge adds
        // AdditionalCoatLaborPercent % of the base subtotal.
        int additionalCoatCount = 0;
        if (item.Coats != null)
        {
            for (int i = 1; i < item.Coats.Count; i++)
            {
                if (!item.Coats[i].NoExtraLayerCharge)
                    additionalCoatCount++;
            }
        }

        var additionalCoatCharge = baseSubtotal * additionalCoatCount * (costs.AdditionalCoatLaborPercent / 100m);
        var itemSubtotal = baseSubtotal + additionalCoatCharge;

        _logger.LogInformation("Additional coats: {Count} × {Pct}% of ${Base} = ${Charge}; Total=${Total}",
            additionalCoatCount, costs.AdditionalCoatLaborPercent, baseSubtotal, additionalCoatCharge, itemSubtotal);

        // ── Step 3: apply complexity multiplier (calculated items only) ────────
        if (!isCatalogItem && !item.IsGenericItem && !item.IsLaborItem)
        {
            var complexityPercent = item.Complexity switch
            {
                "Moderate" => costs.ComplexityModeratePercent,
                "Complex"  => costs.ComplexityComplexPercent,
                "Extreme"  => costs.ComplexityExtremePercent,
                _          => costs.ComplexitySimplePercent   // "Simple" or null
            };

            if (complexityPercent > 0)
            {
                var complexityCharge = itemSubtotal * (complexityPercent / 100m);
                _logger.LogInformation("Complexity '{Complexity}': +{Pct}% = +${Charge}",
                    item.Complexity ?? "Simple", complexityPercent, complexityCharge);
                itemSubtotal += complexityCharge;
            }
        }

        var unitPrice = item.Quantity > 0 ? itemSubtotal / item.Quantity : 0m;
        var totalPrice = itemSubtotal;

        return new QuoteItemPricingResult
        {
            MaterialCost  = totalMaterialCost,
            LaborCost     = totalLaborCost,
            EquipmentCost = totalEquipmentCost,
            ItemSubtotal  = itemSubtotal,
            UnitPrice     = unitPrice,
            TotalPrice    = totalPrice
        };
    }

    /// <summary>
    /// Calculates the complete quote total from all line items, applying quote-level charges,
    /// discounts, and tax. The calculation sequence is strictly ordered (steps 1–14) and must
    /// not be reordered, as each step feeds the next:
    ///
    ///   1.  Sum item subtotals (via <see cref="CalculateQuoteItemPriceAsync"/> per item).
    ///   2.  Add oven batch cost (batches × cycle hours × oven rate) — quote-level, not per-item.
    ///       AI items are excluded from the oven charge by scaling with the non-AI surface-area fraction.
    ///   3.  Add shop supplies charge (ShopSuppliesRate % of items + oven subtotal).
    ///   4.  Apply customer pricing-tier discount (percentage off subtotal before any quote discount).
    ///   5.  Apply quote-level discount (percentage or fixed amount, applied after tier discount).
    ///   6.  Apply rush fee (percentage or fixed amount from operating costs, if IsRushJob).
    ///   7.  Apply tax (manualTaxPercent if provided — used for tax-exempt customers — else CompanyTaxPercent).
    ///   8.  Round all values to 2 decimal places.
    ///
    /// Overhead and profit margin are NOT added here — they were removed as separate line items.
    /// Markup is now baked into per-item prices (step 1). The ProfitPercent field in the result
    /// is the markup % from operating costs, kept for the breakdown display only.
    /// </summary>
    public async Task<QuotePricingResult> CalculateQuoteTotalsAsync(
        List<CreateQuoteItemDto> items,
        int companyId,
        int? customerId = null,
        decimal? manualTaxPercent = null,
        string discountType = "None",
        decimal discountValue = 0,
        bool isRushJob = false,
        decimal? ovenCostOverride = null,
        int ovenBatches = 1,
        int? ovenCycleMinutes = null)
    {
        var costs = await GetOperatingCostsAsync(companyId);
        if (costs == null)
        {
            _logger.LogWarning("No operating costs configured for company {CompanyId}, returning zero pricing", companyId);
            return new QuotePricingResult
            {
                ItemsSubtotal = 0,
                ShopSuppliesAmount = 0,
                ShopSuppliesPercent = 0,
                FacilityOverheadCost = 0,
                FacilityOverheadRatePerHour = 0,
                OverheadCosts = 0,
                OverheadPercent = 0,
                ProfitMargin = 0,
                ProfitPercent = 0,
                SubtotalBeforeDiscount = 0,
                DiscountAmount = 0,
                DiscountPercent = 0,
                SubtotalAfterDiscount = 0,
                TaxAmount = 0,
                TaxPercent = 0,
                Total = 0,
                MaterialCosts = 0,
                LaborCosts = 0,
                EquipmentCosts = 0
            };
        }

        // Calculate all items
        var itemResults = new List<QuoteItemPricingResult>();
        foreach (var item in items)
        {
            QuoteItemPricingResult itemResult;

            // All items (catalog and calculated) go through CalculateQuoteItemPriceAsync, which
            // handles PowderCostOverride, prep cost inclusion, and all item type variants.
            itemResult = await CalculateQuoteItemPriceAsync(item, companyId, ovenCostOverride);

            itemResults.Add(itemResult);
        }

        // 1. SEPARATE CATALOG ITEMS FROM CALCULATED ITEMS
        // Catalog items WITHOUT coats are final prices - no breakdown
        // Catalog items WITH coats should include their cost breakdown
        var catalogItemsWithoutCoatsTotal = items
            .Where(i => i.CatalogItemId.HasValue && (i.Coats == null || !i.Coats.Any()))
            .Zip(itemResults.Where((r, idx) => items[idx].CatalogItemId.HasValue && (items[idx].Coats == null || !items[idx].Coats.Any())), (item, result) => result.ItemSubtotal)
            .Sum();

        // Include calculated items AND catalog items with coats in the breakdown
        var itemsWithBreakdown = itemResults
            .Where((r, idx) => !items[idx].CatalogItemId.HasValue || (items[idx].Coats != null && items[idx].Coats.Any()))
            .ToList();

        var calculatedItemsSubtotal = itemsWithBreakdown.Sum(r => r.ItemSubtotal);
        var totalMaterialCosts = itemsWithBreakdown.Sum(r => r.MaterialCost);
        var totalLaborCosts = itemsWithBreakdown.Sum(r => r.LaborCost);
        var totalEquipmentCosts = itemsWithBreakdown.Sum(r => r.EquipmentCost);

        // 2. OVERHEAD COSTS (removed - was calculated separately)
        var overheadCosts = 0m;

        // 3. PROFIT MARGIN (now baked into item prices, not added separately)
        // Store whichever % was active so the breakdown display shows the right label.
        var profitPercent = costs.PricingMode == PowderCoating.Core.Enums.PricingMode.MarginOnTotalCost
            ? costs.TargetMarginPercent
            : costs.GeneralMarkupPercentage;
        var profitMargin = 0m; // Already included in item prices

        // 4. TOTAL ITEMS SUBTOTAL
        var itemsSubtotal = catalogItemsWithoutCoatsTotal + calculatedItemsSubtotal;

        // Powder-to-order costs are excluded from individual item prices and collected in a
        // "Custom Powder Order" line item added at save time. For live pricing previews (before
        // save), add them back here so the displayed total stays correct throughout the session.
        // Two coat types qualify: custom powder (no InventoryItemId, manual PowderCostPerLb) and
        // incoming powder (InventoryItemId set, IsIncoming=true, cost from inventoryItem.UnitCost).
        bool hasCustomPowderOrderItem = items.Any(i =>
            i.IsGenericItem && i.Description?.StartsWith("Custom Powder Order") == true);
        decimal customPowderOrderAmount = 0m;
        var customPowderOrderColors = new List<string>();
        if (!hasCustomPowderOrderItem)
        {
            foreach (var item in items.Where(i => i.Coats != null))
            {
                foreach (var c in item.Coats!)
                {
                    if (!c.InventoryItemId.HasValue &&
                        c.PowderToOrder.HasValue && c.PowderToOrder.Value > 0 &&
                        c.PowderCostPerLb.HasValue && c.PowderCostPerLb.Value > 0)
                    {
                        customPowderOrderAmount += c.PowderToOrder.Value * c.PowderCostPerLb.Value;
                        if (!string.IsNullOrWhiteSpace(c.ColorName))
                            customPowderOrderColors.Add(c.ColorName);
                    }
                    else if (c.InventoryItemId.HasValue && c.PowderToOrder.HasValue && c.PowderToOrder.Value > 0)
                    {
                        var invItem = await _unitOfWork.InventoryItems.GetByIdAsync(c.InventoryItemId.Value);
                        if (invItem?.IsIncoming == true)
                        {
                            // Bill the powder-to-order at the current catalog price when linked.
                            customPowderOrderAmount += c.PowderToOrder.Value * (invItem.CatalogReferencePrice ?? invItem.UnitCost);
                            var colorName = !string.IsNullOrWhiteSpace(c.ColorName) ? c.ColorName : invItem.Name;
                            if (!string.IsNullOrWhiteSpace(colorName))
                                customPowderOrderColors.Add(colorName);
                        }
                    }
                }
            }
            if (customPowderOrderAmount > 0)
            {
                itemsSubtotal += customPowderOrderAmount;
                totalMaterialCosts += customPowderOrderAmount;
            }
        }

        // 4b. OVEN BATCH COST (quote-level: batches × cycle time × oven rate)
        // AI items already have oven cost baked into their AI-estimated price, so we only
        // charge the proportion of the oven that's attributable to non-AI items.
        var effectiveOvenRate = ovenCostOverride ?? costs.OvenOperatingCostPerHour;
        var effectiveCycleMinutes = ovenCycleMinutes.HasValue && ovenCycleMinutes.Value > 0
            ? ovenCycleMinutes.Value
            : costs.DefaultOvenCycleMinutes;
        var effectiveBatches = Math.Max(1, ovenBatches);
        var fullOvenBatchCost = effectiveBatches * (effectiveCycleMinutes / 60m) * effectiveOvenRate;

        // Only items with coating layers go in the oven — sandblast/prep-only items (zero coats) don't.
        // Of those coating items, AI items already have oven cost baked into their AI price.
        var coatingItems   = items.Where(i => i.Coats != null && i.Coats.Any()).ToList();
        var nonAiCoatItems = coatingItems.Where(i => !i.IsAiItem).ToList();

        decimal nonAiFraction;
        if (!coatingItems.Any())
        {
            nonAiFraction = 0m; // No coated items — no oven charge
        }
        else
        {
            var totalCoatSqFt = coatingItems.Sum(i => i.SurfaceAreaSqFt * i.Quantity);
            var nonAiCoatSqFt = nonAiCoatItems.Sum(i => i.SurfaceAreaSqFt * i.Quantity);
            if (totalCoatSqFt > 0)
                nonAiFraction = nonAiCoatSqFt / totalCoatSqFt;
            else
                nonAiFraction = coatingItems.Count > 0 ? (decimal)nonAiCoatItems.Count / coatingItems.Count : 1m;
        }

        var ovenBatchCost = fullOvenBatchCost * nonAiFraction;

        _logger.LogInformation(
            "Oven batch cost: {Batches} × {Min}min × ${Rate}/hr = ${Full}; non-AI fraction {Frac:P0} → charged ${Cost}",
            effectiveBatches, effectiveCycleMinutes, effectiveOvenRate, fullOvenBatchCost, nonAiFraction, ovenBatchCost);

        var itemsAndOvenSubtotal = itemsSubtotal + ovenBatchCost;

        // 5. FACILITY OVERHEAD (rent + utilities apportioned by estimated job hours)
        var facilityOverheadRatePerHour = 0m;
        var facilityOverheadCost = 0m;
        if (costs.MonthlyBillableHours > 0 && (costs.MonthlyRent + costs.MonthlyUtilities) > 0)
        {
            facilityOverheadRatePerHour = (costs.MonthlyRent + costs.MonthlyUtilities) / costs.MonthlyBillableHours;
            var totalEstimatedMinutes = items.Sum(i => (decimal)i.EstimatedMinutes * i.Quantity);
            facilityOverheadCost = facilityOverheadRatePerHour * (totalEstimatedMinutes / 60m);
            _logger.LogInformation(
                "Facility overhead: ${Rate:F2}/hr × {Min:F0} min = ${Cost:F2}",
                facilityOverheadRatePerHour, totalEstimatedMinutes, facilityOverheadCost);
        }

        // 6. SHOP SUPPLIES (percentage of items + oven subtotal — does not include facility overhead)
        var shopSuppliesPercent = costs.ShopSuppliesRate;
        var shopSuppliesAmount = itemsAndOvenSubtotal * (shopSuppliesPercent / 100m);

        // 7. SUBTOTAL BEFORE DISCOUNT (items + oven + facility overhead + shop supplies)
        var subtotalBeforeDiscount = itemsAndOvenSubtotal + facilityOverheadCost + shopSuppliesAmount;

        // 8. CUSTOMER PRICING TIER DISCOUNT (if applicable)
        var pricingTierDiscountPercent = 0m;
        var pricingTierDiscountAmount = 0m;

        if (customerId.HasValue)
        {
            try
            {
                var customers = await _unitOfWork.Customers.FindAsync(c => c.Id == customerId.Value);
                var customer = customers.FirstOrDefault();

                if (customer != null)
                {
                    // Get pricing tier if assigned
                    if (customer.PricingTierId.HasValue)
                    {
                        var pricingTier = await _unitOfWork.PricingTiers.GetByIdAsync(customer.PricingTierId.Value);
                        if (pricingTier != null)
                        {
                            pricingTierDiscountPercent = pricingTier.DiscountPercent;
                            pricingTierDiscountAmount = subtotalBeforeDiscount * (pricingTierDiscountPercent / 100m);
                        }
                    }
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "Error retrieving customer {CustomerId} for discount calculation", customerId.Value);
            }
        }

        var subtotalAfterTierDiscount = subtotalBeforeDiscount - pricingTierDiscountAmount;

        // 8. QUOTE-LEVEL DISCOUNT (applied after pricing tier discount)
        var quoteDiscountPercent = 0m;
        var quoteDiscountAmount = 0m;

        if (discountType != "None" && discountValue > 0)
        {
            if (discountType == "Percentage")
            {
                quoteDiscountPercent = discountValue;
                quoteDiscountAmount = subtotalAfterTierDiscount * (discountValue / 100m);
            }
            else if (discountType == "FixedAmount")
            {
                quoteDiscountAmount = discountValue;
                // Calculate what percentage this is for display purposes
                if (subtotalAfterTierDiscount > 0)
                {
                    quoteDiscountPercent = (discountValue / subtotalAfterTierDiscount) * 100m;
                }
            }
        }

        // 9. COMBINED DISCOUNT (pricing tier + quote-level)
        var totalDiscountPercent = pricingTierDiscountPercent + quoteDiscountPercent;
        var totalDiscountAmount = pricingTierDiscountAmount + quoteDiscountAmount;

        // 10. SUBTOTAL AFTER ALL DISCOUNTS
        var subtotalAfterDiscount = subtotalAfterTierDiscount - quoteDiscountAmount;

        // 11. RUSH FEE (if applicable)
        var rushFee = 0m;
        if (isRushJob)
        {
            if (costs.RushChargeType == "Percentage")
            {
                rushFee = subtotalAfterDiscount * (costs.RushChargePercentage / 100m);
            }
            else // FixedAmount
            {
                rushFee = costs.RushChargeFixedAmount;
            }
        }

        // 12. SUBTOTAL INCLUDING RUSH FEE
        var subtotalWithRushFee = subtotalAfterDiscount + rushFee;

        // 13. TAX
        var taxPercent = manualTaxPercent ?? costs.TaxPercent;
        var taxAmount = subtotalWithRushFee * (taxPercent / 100m);

        // 14. FINAL TOTAL
        var total = subtotalWithRushFee + taxAmount;

        return new QuotePricingResult
        {
            ItemsSubtotal = Math.Round(itemsSubtotal, 2),
            OvenBatchCost = Math.Round(ovenBatchCost, 2),
            OvenBatches = effectiveBatches,
            OvenCycleMinutes = effectiveCycleMinutes,
            FacilityOverheadCost = Math.Round(facilityOverheadCost, 2),
            FacilityOverheadRatePerHour = Math.Round(facilityOverheadRatePerHour, 4),
            ShopSuppliesAmount = Math.Round(shopSuppliesAmount, 2),
            ShopSuppliesPercent = Math.Round(shopSuppliesPercent, 2),
            OverheadCosts = Math.Round(overheadCosts, 2),
            OverheadPercent = 0m, // Overhead removed
            ProfitMargin = Math.Round(profitMargin, 2), // 0 - now baked into item prices
            ProfitPercent = Math.Round(profitPercent, 2), // Markup % used (for reference)
            SubtotalBeforeDiscount = Math.Round(subtotalBeforeDiscount, 2),

            // Separate discounts
            PricingTierDiscountAmount = Math.Round(pricingTierDiscountAmount, 2),
            PricingTierDiscountPercent = Math.Round(pricingTierDiscountPercent, 2),
            QuoteDiscountAmount = Math.Round(quoteDiscountAmount, 2),
            QuoteDiscountPercent = Math.Round(quoteDiscountPercent, 2),

            // Combined discount (for backward compatibility)
            DiscountAmount = Math.Round(totalDiscountAmount, 2),
            DiscountPercent = Math.Round(totalDiscountPercent, 2),

            SubtotalAfterDiscount = Math.Round(subtotalAfterDiscount, 2),
            RushFee = Math.Round(rushFee, 2),
            TaxAmount = Math.Round(taxAmount, 2),
            TaxPercent = Math.Round(taxPercent, 2),
            Total = Math.Round(total, 2),
            MaterialCosts = Math.Round(totalMaterialCosts, 2),
            LaborCosts = Math.Round(totalLaborCosts, 2),
            EquipmentCosts = Math.Round(totalEquipmentCosts, 2),
            ItemResults = itemResults,
            CustomPowderOrderAmount = Math.Round(customPowderOrderAmount, 2),
            CustomPowderOrderColors = customPowderOrderColors
                .Distinct(StringComparer.OrdinalIgnoreCase)
                .ToList()
        };
    }
}
