package com.ffii.fpsms.modules.stock.entity

import com.ffii.core.support.AbstractRepository
import com.ffii.fpsms.modules.stock.entity.projection.CurrentInventoryItemInfo
import com.ffii.fpsms.modules.stock.entity.projection.InventoryLotLineInfo
import org.springframework.data.domain.Page
import org.springframework.data.domain.Pageable
import org.springframework.data.jpa.repository.Modifying
import org.springframework.data.jpa.repository.Query
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional
import java.io.Serializable
import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus
import org.springframework.data.repository.query.Param
import java.time.LocalDate
import org.springframework.data.jpa.repository.EntityGraph
import java.math.BigDecimal
@Repository
interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long> {

    /**
     * Workbench pick: one conditional atomic UPDATE — winner updates row, loser gets 0 rows.
     * - Requires expected [BaseEntity.version] (optimistic row lock).
     * - No-hold rule: pickable only if (in - out) >= delta (ignore holdQty).
     * - Recomputes [InventoryLotLine.status] in SQL (same rules as [InventoryLotLineService.deriveInventoryLotLineStatus]).
     * - Bumps version in SQL so no stale JPA save is needed on this row afterward.
     */
    
    @Modifying(clearAutomatically = true, flushAutomatically = true)
    @Transactional
    @Query(
        value = """
         UPDATE inventory_lot_line ill
SET ill.outQty = COALESCE(ill.outQty, 0) + :delta,
    ill.status = CASE
        WHEN (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) > 0
        THEN 'available'
        ELSE 'unavailable'
    END,
    ill.version = ill.version + 1,
    ill.modified = CURRENT_TIMESTAMP
WHERE ill.id = :id
  AND ill.deleted = 0
  AND ill.version = :expectedVersion
  AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) >= :delta
        """,
        nativeQuery = true
    )
    fun incrementOutQtyIfAvailable(
        @Param("id") id: Long,
        @Param("delta") delta: BigDecimal,
        @Param("expectedVersion") expectedVersion: Int,
    ): Int

    fun findByIdAndDeletedIsFalse(id: Serializable): InventoryLotLine;

    fun findInventoryLotLineInfoByInventoryLotItemIdIn(ids: List<Serializable>): List<InventoryLotLineInfo>

    @Query("select ill from InventoryLotLine ill where :id is null or ill.inventoryLot.item.id = :id order by ill.id desc")
    fun findInventoryLotLineInfoByItemId(id: Long?, pageable: Pageable): Page<InventoryLotLineInfo>

    @Query("""
        select 
            i.id as id,
            i.code as itemCode,
            i.name as itemName,
            sum(coalesce(ill.inQty, 0) - coalesce(ill.outQty, 0) - coalesce(ill.holdQty, 0)) as availableQty
        from InventoryLotLine ill 
        left join InventoryLot il on ill.inventoryLot = il
        left join Items i on il.item = i
        where i.id in :items
        group by i.id
    """)
    fun findCurrentInventoryByItems(items: List<Serializable>): List<CurrentInventoryItemInfo>

    fun findAllByIdIn(ids: List<Serializable>): List<InventoryLotLine>

    fun findAllByInventoryLotId(id: Serializable): List<InventoryLotLine>

    fun findByInventoryLotStockInLineIdAndWarehouseId(inventoryLotStockInLineId: Long, warehouseId: Long): InventoryLotLine?

    fun findAllByInventoryLotItemIdAndStatus(itemId: Long, status: String): List<InventoryLotLine>
    fun findAllByInventoryLotItemIdAndStatus(itemId: Long, status: InventoryLotLineStatus): List<InventoryLotLine> 
    @Query("SELECT ill FROM InventoryLotLine ill WHERE ill.inventoryLot.item.id IN :itemIds")
    fun findAllByItemIdIn(@Param("itemIds") itemIds: List<Long>): List<InventoryLotLine>
    @Query("""
    SELECT ill FROM InventoryLotLine ill 
    WHERE ill.inventoryLot.lotNo = :lotNo 
    AND ill.inventoryLot.item.id = :itemId
    AND ill.deleted = false
    """)
    fun findByLotNoAndItemId(lotNo: String, itemId: Long): InventoryLotLine?

    @Query("""
        SELECT DISTINCT ill.inventoryLot.item.id
        FROM InventoryLotLine ill
        WHERE ill.deleted = false
          AND ill.inventoryLot.lotNo = :lotNo
    """)
    fun findDistinctItemIdsByLotNo(@Param("lotNo") lotNo: String): List<Long>

    // lotNo + itemId may not be unique (multiple warehouses/lines); pick one deterministically
    fun findFirstByInventoryLotLotNoAndInventoryLotItemIdAndDeletedFalseOrderByIdDesc(
        lotNo: String,
        itemId: Long
    ): InventoryLotLine?

    @EntityGraph(
        type = EntityGraph.EntityGraphType.FETCH,
        attributePaths = [
            "inventoryLot", 
            "inventoryLot.item",     // important: two levels deep
            "warehouse", 
            "stockUom",
            "stockUom.uom"           // if uom is a relation inside ItemUom
        ]
    )
    @Query("SELECT ill FROM InventoryLotLine ill WHERE ill.warehouse.id IN :warehouseIds AND ill.deleted = false")
    fun findAllByWarehouseIdInAndDeletedIsFalse(@Param("warehouseIds") warehouseIds: List<Long>): List<InventoryLotLine>
    
    // You can also add another one for the status filter if needed
    @EntityGraph(
        type = EntityGraph.EntityGraphType.FETCH,
        attributePaths = ["inventoryLot", "inventoryLot.item", "warehouse", "stockUom", "stockUom.uom"]
    )
    @Query("""
        SELECT ill 
        FROM InventoryLotLine ill 
        WHERE ill.warehouse.id IN :warehouseIds 
          AND ill.deleted = false 
          AND ill.status = 'available'
    """)
    fun findAllByWarehouseIdInAndDeletedIsFalseAndStatusIsAvailable(@Param("warehouseIds") warehouseIds: List<Long>): List<InventoryLotLine>
    @EntityGraph(
        type = EntityGraph.EntityGraphType.FETCH,
        attributePaths = ["inventoryLot", "inventoryLot.item", "warehouse", "stockUom", "stockUom.uom"]
    )
    @Query("""
    SELECT ill FROM InventoryLotLine ill
    WHERE ill.deleted = false
      AND ill.warehouse.id IN :warehouseIds
      AND ill.inventoryLot.id IN :inventoryLotIds
    """)
    fun findAllByWarehouseIdInAndInventoryLotIdInAndDeletedIsFalse(
        @Param("warehouseIds") warehouseIds: Collection<Long>,
        @Param("inventoryLotIds") inventoryLotIds: Collection<Long>
    ): List<InventoryLotLine>

    @Query("""
    SELECT ill FROM InventoryLotLine ill 
    WHERE ill.warehouse.code = :warehouseCode 
    AND ill.deleted = false
    ORDER BY ill.inventoryLot.item.code, ill.inventoryLot.lotNo
""")
    fun findAllByWarehouseCodeAndDeletedIsFalse(@Param("warehouseCode") warehouseCode: String): List<InventoryLotLine>


    @Query("""
    SELECT COUNT(DISTINCT ill.inventoryLot.item.id) 
    FROM InventoryLotLine ill 
    WHERE ill.warehouse.id IN :warehouseIds 
    AND ill.deleted = false
""")
    fun countDistinctItemsByWarehouseIds(@Param("warehouseIds") warehouseIds: List<Long>): Long

    @Query("""
    SELECT COUNT(ill.id) 
    FROM InventoryLotLine ill 
    WHERE ill.warehouse.id IN :warehouseIds 
    AND ill.deleted = false
""")
    fun countAllByWarehouseIds(@Param("warehouseIds") warehouseIds: List<Long>): Long

    @EntityGraph(
        type = EntityGraph.EntityGraphType.FETCH,
        attributePaths = ["inventoryLot", "inventoryLot.item", "warehouse"]
    )
    @Query("""
    SELECT ill FROM InventoryLotLine ill
    JOIN ill.inventoryLot il
    JOIN il.item i
    WHERE il.expiryDate < :today
      AND (:expiryDate IS NULL OR il.expiryDate = :expiryDate)
      AND (:itemCode IS NULL OR LOWER(i.code) LIKE LOWER(CONCAT('%', :itemCode, '%')))
      AND (:itemName IS NULL OR LOWER(i.name) LIKE LOWER(CONCAT('%', :itemName, '%')))
      AND coalesce(ill.inQty, 0) <> coalesce(ill.outQty, 0)
      AND ill.deleted = false
      AND il.deleted = false
      AND i.deleted = false
    ORDER BY il.expiryDate ASC
""")
    fun findExpiredItems(
        @Param("today") today: LocalDate,
        @Param("expiryDate") expiryDate: LocalDate?,
        @Param("itemCode") itemCode: String?,
        @Param("itemName") itemName: String?,
    ): List<InventoryLotLine>

    /**
     * Same business key as transfer merge target lookup; restricted to [status] (e.g. AVAILABLE).
     * Returns a list — never single-result — so duplicate unavailable rows elsewhere do not cause NonUniqueResultException.
     */
    @Query("""
    SELECT ill FROM InventoryLotLine ill 
    WHERE ill.inventoryLot.lotNo = :lotNo 
    AND ill.inventoryLot.item.id = :itemId
    AND ill.warehouse.id = :warehouseId
    AND ill.deleted = false
    AND ill.status = :status
    ORDER BY ill.id ASC
    """)
    fun findAllByLotNoAndItemIdAndWarehouseIdAndStatus(
        @Param("lotNo") lotNo: String,
        @Param("itemId") itemId: Long,
        @Param("warehouseId") warehouseId: Long,
        @Param("status") status: InventoryLotLineStatus,
    ): List<InventoryLotLine>

    @Query("""
    SELECT ill FROM InventoryLotLine ill 
    WHERE ill.deleted = false 
    AND ill.inventoryLot.stockInLine IS NOT NULL
    ORDER BY ill.inventoryLot.lotNo
""")
    fun findAllByDeletedIsFalseAndHasStockInLine(): List<InventoryLotLine>

    @Query("""
    SELECT ill FROM InventoryLotLine ill
    JOIN ill.warehouse w
    JOIN ill.inventoryLot il
    WHERE w.store_id IN :storeIds
      AND ill.deleted = false
      AND il.stockInLine IS NOT NULL
    ORDER BY w.store_id, il.item.code, il.lotNo
""")
    fun findAllByStoreIdsAndHasStockInLine(
        @Param("storeIds") storeIds: List<String>
    ): List<InventoryLotLine>

    @Modifying
    @Transactional
    @Query(
        value = """
            UPDATE inventory_lot_line ill
            INNER JOIN inventory_lot il ON il.id = ill.inventoryLotId
            SET ill.status = 'unavailable'
            WHERE il.deleted = 0
              AND ill.deleted = 0
              AND il.expiryDate IS NOT NULL
              AND il.expiryDate < CURRENT_DATE
              AND (ill.status IS NULL OR LOWER(ill.status) <> 'unavailable')
        """,
        nativeQuery = true
    )
    fun markExpiredLotLinesAsUnavailable(): Int

    /** 對應 SQL: COUNT(*) FROM inventory_lot_line WHERE status = 'available'（另排除已刪除列）。 */
    @Query(
        """
        SELECT COUNT(ill) FROM InventoryLotLine ill
        WHERE ill.status = :status AND ill.deleted = false
        """
    )
    fun countByStatusAndDeletedIsFalse(@Param("status") status: InventoryLotLineStatus): Long

    @EntityGraph(
    type = EntityGraph.EntityGraphType.FETCH,
    attributePaths = ["inventoryLot", "inventoryLot.item", "warehouse"]
)
@Query("""
    SELECT ill
    FROM InventoryLotLine ill
    WHERE ill.id IN :ids
      AND ill.deleted = false
""")
fun findAllByIdInAndDeletedFalseWithRefs(
    @Param("ids") ids: Collection<Long>
): List<InventoryLotLine>
}