package com.ffii.fpsms.modules.master.service

import com.ffii.fpsms.modules.master.entity.ItemUom
import com.ffii.fpsms.modules.master.entity.ItemUomRespository
import com.ffii.fpsms.modules.master.web.models.*
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.io.IOException
import java.math.BigDecimal
import java.math.RoundingMode
import kotlin.jvm.optionals.getOrNull
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import com.ffii.fpsms.modules.master.entity.ItemsRepository
import com.ffii.fpsms.modules.stock.entity.Inventory
import com.ffii.fpsms.modules.stock.entity.InventoryRepository

@Service
open class ItemUomService(

    val uomConversionService: UomConversionService,
    val itemUomRespository: ItemUomRespository,
    val currencyService: CurrencyService,
    val itemsRepository: ItemsRepository,
    private val inventoryRepository: InventoryRepository,
) {
    val logger = org.slf4j.LoggerFactory.getLogger(this::class.java)

    open fun findAllByItemsId(itemId: Long): List<ItemUom> {
        return itemUomRespository.findAllByItemIdAndDeletedIsFalse(itemId);
    }

    open fun findById(id: Long): ItemUom? {
        return itemUomRespository.findByIdAndDeletedIsFalse(id);
    }

    open fun findByM18Id(m18Id: Long): ItemUom? {
        return itemUomRespository.findByM18IdAndDeletedIsFalse(m18Id);
    }

    open fun findPurchaseUnitByItemId(itemId: Long): ItemUom? {
        return itemUomRespository.findByItemIdAndPurchaseUnitIsTrueAndDeletedIsFalse(itemId)
    }

    open fun findSalesUnitByItemId(itemId: Long): ItemUom? {
        return itemUomRespository.findByItemIdAndSalesUnitIsTrueAndDeletedIsFalse(itemId)
    }
    open fun findStockUnitByItemId(itemId: Long): ItemUom? {
        return itemUomRespository.findByItemIdAndStockUnitIsTrueAndDeletedIsFalse(itemId)
    }

    open fun findFirstByItemIdAndUomId(itemId: Long, uomId: Long): ItemUom? {
        return itemUomRespository.findFirstByItemIdAndUomIdAndDeletedIsFalse(itemId, uomId)
    }

    open fun findByItemIdAndUomIdAndStockUnit(itemId: Long, uomId: Long, stockUnit: Boolean): ItemUom? {
        return itemUomRespository.findByItemIdAndUomIdAndStockUnitAndDeletedIsFalse(itemId, uomId, stockUnit)
    }

    open fun findDistinctProblemItemM18IdsForUomResync(): List<Long> {
        return itemUomRespository.findDistinctProblemItemM18IdsForUomResync()
    }

    open fun findPurchaseUnitByM18ItemId(m18ItemId: Long): ItemUom? {
        return itemUomRespository.findByItemM18IdAndPurchaseUnitIsTrueAndDeletedIsFalse(m18ItemId)
    }

    open fun findBaseUnitByItemId(itemId: Long): ItemUom? {
        return itemUomRespository.findByItemIdAndBaseUnitIsTrueAndDeletedIsFalse(itemId)
    }

    /**
     * 同一 [itemId] 可能對應多筆 [Inventory]（不同 uom）。
     * 優先取 `inventory.uomId` 與 `item_uom` 中 `baseUnit = true` 之列的 `uomId` 一致的那一筆；
     * 若無 base 設定或無匹配列，則回退為 `findFirstByItemIdAndDeletedIsFalseOrderByIdAsc`。
     */
    open fun findInventoryForItemBaseUom(itemId: Long): Inventory? {
        val base = findBaseUnitByItemId(itemId)
        val uomId = base?.uom?.id
        if (uomId != null) {
            val match = inventoryRepository.findFirstByItemIdAndUomIdAndDeletedIsFalseOrderByIdAsc(itemId, uomId)
            if (match != null) {
                return match
            }
        }
        return inventoryRepository.findFirstByItemIdAndDeletedIsFalseOrderByIdAsc(itemId)
    }

    open fun findPickingUnitByItemId(itemId: Long): ItemUom? {
        return itemUomRespository.findByItemIdAndPickingUnitIsTrueAndDeletedIsFalse(itemId)
    }

    private fun findTargetItemUom(itemId: Long, targetUnit: String): ItemUom? {
        return when (targetUnit.lowercase()) {
            "baseunit", "base_unit" -> findBaseUnitByItemId(itemId)
            "stockunit", "stock_unit" -> findStockUnitByItemId(itemId)
            "purchaseunit", "purchase_unit" -> findPurchaseUnitByItemId(itemId)
            "salesunit", "sales_unit" -> findSalesUnitByItemId(itemId)
            "pickingunit", "picking_unit" -> findPickingUnitByItemId(itemId)
            else -> null
        }
    }

    open fun convertPurchaseQtyToStockQty(itemId: Long, purchaseQty: BigDecimal): BigDecimal {
        val purchaseUnit = findPurchaseUnitByItemId(itemId) ?: return purchaseQty;
        val stockUnit = findStockUnitByItemId(itemId) ?: return purchaseQty;
        val one = BigDecimal.ONE;

        // IMPORTANT:
        // Use high precision for intermediate steps, and round to integer only at the end.
        // Rounding during intermediate steps (scale=0 + UP) can cause drift (e.g. 2 -> 11 -> 3).
        val calcScale = 10

        val baseQty = purchaseQty
            .multiply(purchaseUnit.ratioN ?: one)
            .divide(purchaseUnit.ratioD ?: one, calcScale, RoundingMode.HALF_UP)

        val stockQty = baseQty
            .multiply(stockUnit.ratioD ?: one)
            .divide(stockUnit.ratioN ?: one, calcScale, RoundingMode.HALF_UP)

        return stockQty.setScale(0, RoundingMode.UP)
    }

    /**
     * Convert purchase qty -> stock qty (PO display / acceptedQty calculation)
     * with round-down to integer stock qty.
     */
    open fun convertPurchaseQtyToStockQtyRoundDown(itemId: Long, purchaseQty: BigDecimal): BigDecimal {
        val purchaseUnit = findPurchaseUnitByItemId(itemId) ?: return purchaseQty
        val stockUnit = findStockUnitByItemId(itemId) ?: return purchaseQty
        val one = BigDecimal.ONE

        // Use high precision for intermediate steps, and round down only at the end.
        val calcScale = 10

        val baseQty = purchaseQty
            .multiply(purchaseUnit.ratioN ?: one)
            .divide(purchaseUnit.ratioD ?: one, calcScale, RoundingMode.HALF_UP)

        val stockQty = baseQty
            .multiply(stockUnit.ratioD ?: one)
            .divide(stockUnit.ratioN ?: one, calcScale, RoundingMode.HALF_UP)

        return stockQty.setScale(0, RoundingMode.DOWN)
    }

    /** Inverse of convertPurchaseQtyToStockQty: stock qty -> purchase qty (for PO-origin StockInLine display). */
    open fun convertStockQtyToPurchaseQty(itemId: Long, stockQty: BigDecimal): BigDecimal {
        val purchaseUnit = findPurchaseUnitByItemId(itemId) ?: return stockQty
        val stockUnit = findStockUnitByItemId(itemId) ?: return stockQty
        val one = BigDecimal.ONE
        val calcScale = 10

        val baseQty = stockQty
            .multiply(stockUnit.ratioN ?: one)
            .divide(stockUnit.ratioD ?: one, calcScale, RoundingMode.HALF_UP)

        val purchaseQty = baseQty
            .multiply(purchaseUnit.ratioD ?: one)
            .divide(purchaseUnit.ratioN ?: one, calcScale, RoundingMode.HALF_UP)

        return purchaseQty.setScale(0, RoundingMode.UP)
    }

    /**
     * Convert source quantity from a specific UOM to this item's purchase unit quantity.
     * Returns source qty when source/purchase unit mapping is not found.
     */
    open fun convertQtyToPurchaseQty(itemId: Long, uomId: Long, sourceQty: BigDecimal): BigDecimal {
        val sourceItemUom = findFirstByItemIdAndUomId(itemId, uomId) ?: return sourceQty
        val purchaseUnit = findPurchaseUnitByItemId(itemId) ?: return sourceQty
        val one = BigDecimal.ONE
        val calcScale = 10

        val baseQty = sourceQty
            .multiply(sourceItemUom.ratioN ?: one)
            .divide(sourceItemUom.ratioD ?: one, calcScale, RoundingMode.HALF_UP)

        val purchaseQty = baseQty
            .multiply(purchaseUnit.ratioD ?: one)
            .divide(purchaseUnit.ratioN ?: one, calcScale, RoundingMode.HALF_UP)

        return purchaseQty.setScale(0, RoundingMode.UP)
    }

    open fun convertQtyToStockQty(itemId: Long, uomId: Long, sourceQty: BigDecimal): BigDecimal {
        val itemUom = findFirstByItemIdAndUomId(itemId, uomId) ?: return sourceQty;

        val stockUnit = findStockUnitByItemId(itemId) ?: return BigDecimal.ZERO;
        val one = BigDecimal.ONE;

        val calcScale = 10

        val baseQty = sourceQty
            .multiply(itemUom.ratioN ?: one)
            .divide(itemUom.ratioD ?: one, calcScale, RoundingMode.HALF_UP)

        val stockQty = baseQty
            .multiply(stockUnit.ratioD ?: one)
            .divide(stockUnit.ratioN ?: one, calcScale, RoundingMode.HALF_UP)

        return stockQty.setScale(0, RoundingMode.UP)
    }

    /**
     * Convert source quantity from a specific UOM to this item's stock quantity.
     * Same as convertQtyToStockQty but rounds down the final stock qty to integer.
     */
    open fun convertQtyToStockQtyRoundDown(itemId: Long, uomId: Long, sourceQty: BigDecimal): BigDecimal {
        val itemUom = findFirstByItemIdAndUomId(itemId, uomId) ?: return sourceQty
        val stockUnit = findStockUnitByItemId(itemId) ?: return BigDecimal.ZERO
        val one = BigDecimal.ONE
        val calcScale = 10

        val baseQty = sourceQty
            .multiply(itemUom.ratioN ?: one)
            .divide(itemUom.ratioD ?: one, calcScale, RoundingMode.HALF_UP)

        val stockQty = baseQty
            .multiply(stockUnit.ratioD ?: one)
            .divide(stockUnit.ratioN ?: one, calcScale, RoundingMode.HALF_UP)

        return stockQty.setScale(0, RoundingMode.DOWN)
    }

    /**
     * Stock UOM code [PCS/CTN]* denotes pieces-per-carton; displayed stock qty should be whole cartons (integer).
     */
    private fun isPiecesPerCartonStockUomCode(stockUnit: ItemUom?): Boolean {
        val code = stockUnit?.uom?.code?.trim() ?: return false
        return code.startsWith("PCS/CTN", ignoreCase = true)
    }

    /** After ratio math: 2 decimals, or integer when stock unit is PCS/CTN… */
    private fun finalizePreciseStockQty(stockUnit: ItemUom?, stockQty: BigDecimal): BigDecimal {
        if (isPiecesPerCartonStockUomCode(stockUnit)) {
            return stockQty.setScale(0, RoundingMode.HALF_UP)
        }
        return stockQty.setScale(2, RoundingMode.HALF_UP)
    }

    /**
     * Convert purchase qty -> stock qty and keep decimal precision.
     * Used for PO flows where decimal quantity must be preserved.
     */
    open fun convertPurchaseQtyToStockQtyPrecise(itemId: Long, purchaseQty: BigDecimal): BigDecimal {
        val purchaseUnit = findPurchaseUnitByItemId(itemId) ?: return purchaseQty
        val stockUnit = findStockUnitByItemId(itemId) ?: return purchaseQty
        val one = BigDecimal.ONE
        val calcScale = 10

        val baseQty = purchaseQty
            .multiply(purchaseUnit.ratioN ?: one)
            .divide(purchaseUnit.ratioD ?: one, calcScale, RoundingMode.HALF_UP)

        val stockQty = baseQty
            .multiply(stockUnit.ratioD ?: one)
            .divide(stockUnit.ratioN ?: one, calcScale, RoundingMode.HALF_UP)

        return finalizePreciseStockQty(stockUnit, stockQty)
    }

    /**
     * Convert qty from a specific UOM -> stock qty and keep decimal precision.
     * Used for PO (M18 UOM) conversion where round-down is not allowed.
     */
    open fun convertQtyToStockQtyPrecise(itemId: Long, uomId: Long, sourceQty: BigDecimal): BigDecimal {
        val itemUom = findFirstByItemIdAndUomId(itemId, uomId) ?: return sourceQty
        val stockUnit = findStockUnitByItemId(itemId) ?: return BigDecimal.ZERO
        val one = BigDecimal.ONE
        val calcScale = 10

        val baseQty = sourceQty
            .multiply(itemUom.ratioN ?: one)
            .divide(itemUom.ratioD ?: one, calcScale, RoundingMode.HALF_UP)

        val stockQty = baseQty
            .multiply(stockUnit.ratioD ?: one)
            .divide(stockUnit.ratioN ?: one, calcScale, RoundingMode.HALF_UP)

        return finalizePreciseStockQty(stockUnit, stockQty)
    }

    /**
     * Convert quantity from [uomId] (must exist on `item_uom` for [itemId]) to the item's **base unit** quantity.
     * Returns null when no `item_uom` row links the item to that UOM.
     */
    open fun convertQtyToBaseQtyPrecise(itemId: Long, uomId: Long, sourceQty: BigDecimal): BigDecimal? {
        val itemUom = findFirstByItemIdAndUomId(itemId, uomId) ?: return null
        val one = BigDecimal.ONE
        val calcScale = 10
        return sourceQty
            .multiply(itemUom.ratioN ?: one)
            .divide(itemUom.ratioD ?: one, calcScale, RoundingMode.HALF_UP)
            .stripTrailingZeros()
    }

    // See if need to update the response
    open fun saveItemUom(request: ItemUomRequest): ItemUom {
        val itemUom = request.m18Id?.let { itemUomRespository.findFirstByM18IdOrderByIdDesc(it) }
            ?: request.id?.let { itemUomRespository.findById(it).orElse(null) }
            ?: ItemUom()

        //if (request.m18LastModifyDate == itemUom.m18LastModifyDate) {
        //    logger.info("Skipping update - m18LastModifyDate unchanged")
        //    return itemUom
        //}

        val item = request.itemId?.let { itemsRepository.findById(it).getOrNull() }
    ?: request.itemId?.let { itemsRepository.findByIdAndDeletedFalse(it) }  // 取決於你 repo 有沒有這種方法
        val uom = request.m18UomId?.let { uomConversionService.findByM18Id(it) } ?: request.uomId?.let { uomConversionService.find(it).getOrNull() }
        val currency = request.currencyId?.let { currencyService.findById(it) }

        itemUom.apply {
            this.uom = uom
            this.item = item
            this.currency = currency
            baseUnit = request.baseUnit
            stockUnit = request.stockUnit
            pickingUnit = request.pickingUnit
            salesUnit = request.salesUnit
            purchaseUnit = request.purchaseUnit
            price = request.price
            m18Id = request.m18Id
            m18LastModifyDate = request.m18LastModifyDate
            ratioD = request.ratioD
            ratioN = request.ratioN
            itemRatioD = request.itemRatioD
            itemRatioN = request.itemRatioN
            deleted = request.deleted  // ← EXPLICIT assignment (same as before)
        }

        val savedItemUom = itemUomRespository.saveAndFlush(itemUom)

        return savedItemUom
    }

    @Throws(IOException::class)
    @Transactional
    open fun deleteItemUoms(deleteIds: List<Long>) {
//        println(deleteIds)
        if (deleteIds.isNotEmpty()) {
            itemUomRespository.deleteAllByIdIn(deleteIds)
        }
    }


    open fun convertUomByItem(request: ConvertUomByItemRequest): ConvertUomByItemResponse {
        // Special case: Direct conversion from KG (784) to gram (4) or vice versa
        // This handles cases where ItemUom records might not exist
        if (request.uomId == 784L && request.targetUnit.lowercase() == "baseunit") {
            // Check if base unit is gram (id=4)
            val baseUnitItemUom = findBaseUnitByItemId(request.itemId)
            val baseUnitUomId = baseUnitItemUom?.uom?.id
            
            if (baseUnitUomId == 4L) {
                // Direct conversion: 1 KG = 1000 grams
                val convertedQty = request.qty.multiply(BigDecimal(1000))
                val gramUom = uomConversionService.findById(4L)
                    ?: throw IllegalArgumentException("UomConversion not found for id=4 (gram)")
                
                return ConvertUomByItemResponse(
                    newQty = convertedQty,
                    udfudesc = gramUom.udfudesc,
                    udfShortDesc = gramUom.udfShortDesc
                )
            }
        }
        
        if (request.uomId == 784L) {
            val targetItemUom = findTargetItemUom(request.itemId, request.targetUnit)
            val targetUomId = targetItemUom?.uom?.id
            
            if (targetUomId == 18L) {
                // Direct conversion: 1 KG = 1000 milliliters (assuming density = 1 g/ml for water-based products)
                // Note: This is a general conversion, actual conversion may vary by product density
                val convertedQty = request.qty.multiply(BigDecimal(1000))
                val milliliterUom = uomConversionService.findById(19L)
                    ?: throw IllegalArgumentException("UomConversion not found for id=19 (milliliter)")
                
                return ConvertUomByItemResponse(
                    newQty = convertedQty,
                    udfudesc = milliliterUom.udfudesc,
                    udfShortDesc = milliliterUom.udfShortDesc
                )
            }
        }
        
        // Find source ItemUom by itemId and uomId
        var sourceItemUom = itemUomRespository.findFirstByItemIdAndUomIdAndDeletedIsFalse(request.itemId, request.uomId)
        
        // Special handling: If source is KG (784) but ItemUom doesn't exist, try indirect conversion via baseUnit
        if (sourceItemUom == null && request.uomId == 784L) {
            val baseUnitItemUom = findBaseUnitByItemId(request.itemId)
            val baseUnitUomId = baseUnitItemUom?.uom?.id
            
            // If base unit is gram (id=4), convert KG -> G first, then G -> target
            if (baseUnitUomId == 4L) {
                // Step 1: Convert KG to G (baseUnit)
                val baseQty = request.qty.multiply(BigDecimal(1000))
                
                // Step 2: Convert from baseUnit to targetUnit
                val targetItemUom = findTargetItemUom(request.itemId, request.targetUnit)
                    ?: throw IllegalArgumentException("Target ItemUom not found for itemId=${request.itemId}, targetUnit=${request.targetUnit}")
                
                val targetRatioN = targetItemUom.ratioN ?: BigDecimal.ONE
                val targetRatioD = targetItemUom.ratioD ?: BigDecimal.ONE
                
                // Convert base (G) to target: baseQty * ratioD / ratioN
                val newQty = baseQty.multiply(targetRatioD).divide(targetRatioN, 2, RoundingMode.UP)
                
                val uomConversion = targetItemUom.uom
                    ?: throw IllegalArgumentException("Target UomConversion not found for target ItemUom")
                
                return ConvertUomByItemResponse(
                    newQty = newQty,
                    udfudesc = uomConversion.udfudesc,
                    udfShortDesc = uomConversion.udfShortDesc
                )
            }
        }
        
       // var sourceItemUom = itemUomRespository.findFirstByItemIdAndUomIdAndDeletedIsFalse(request.itemId, request.uomId)

        // 這裡先查 items、uom_conversion，準備錯誤訊息要用的 code
        val item = itemsRepository.findById(request.itemId).getOrNull()
        val itemCode = item?.code ?: request.itemId.toString()
        
        val uom = uomConversionService.findById(request.uomId)
        val uomCode = uom?.code ?: request.uomId.toString()
        
        // If still no source ItemUom found, throw error
        sourceItemUom ?: throw IllegalArgumentException(
            "Source ItemUom not found for items.code=$itemCode, uom_conversion.code=$uomCode"
        )
        
        // Find target ItemUom by itemId and targetUnit（這段可視需求決定要不要也查 code）
        val targetItemUom = findTargetItemUom(request.itemId, request.targetUnit)
            ?: throw IllegalArgumentException(
                "Target ItemUom not found for items.code=$itemCode, targetUnit=${request.targetUnit}"
            )
        // Convert quantity using ratioN/ratioD via base unit
        val one = BigDecimal.ONE
        val sourceRatioN = sourceItemUom.ratioN ?: one
        val sourceRatioD = sourceItemUom.ratioD ?: one
        val targetRatioN = targetItemUom.ratioN ?: one
        val targetRatioD = targetItemUom.ratioD ?: one
    
        // Convert source qty to base: qty * ratioN / ratioD
        val baseQty = request.qty.multiply(sourceRatioN).divide(sourceRatioD, 2, RoundingMode.UP)
    
        // Convert base to target: baseQty * ratioD / ratioN
        val newQty = baseQty.multiply(targetRatioD).divide(targetRatioN, 2, RoundingMode.UP)
    
        // Get UomConversion from target ItemUom
        val uomConversion = targetItemUom.uom
            ?: throw IllegalArgumentException("Target UomConversion not found for target ItemUom")
    
        return ConvertUomByItemResponse(
            newQty = newQty,
            udfudesc = uomConversion.udfudesc,
            udfShortDesc = uomConversion.udfShortDesc
        )
    }


    
}