import i18next from 'i18next'
import { EntityManager, In, Not } from 'typeorm'
import {
  Worksheet,
  WorksheetDetail,
  WorksheetController,
  WORKSHEET_STATUS,
  WORKSHEET_TYPE
} from '@things-factory/worksheet-base'
import { Domain } from '@things-factory/shell'
import {
  OrderInventory,
  ReleaseGood,
  InventoryCheckItem,
  INVENTORY_CHECK_ITEM_STATUS,
  ORDER_INVENTORY_STATUS,
  ORDER_STATUS
} from '@things-factory/sales-base'
import {
  generateInventoryHistory,
  Inventory,
  InventoryChange,
  Location,
  LOCATION_TYPE,
  INVENTORY_TRANSACTION_TYPE,
  INVENTORY_STATUS
} from '@things-factory/warehouse-base'
import { Product, ProductBarcode, ProductDetail } from '@things-factory/product-base'
import { User } from '@things-factory/auth-base'
import { Setting } from '@things-factory/setting-base'
import { v4 as uuidv4 } from 'uuid'

export const zonePicking = {
  async scanProductZonePicking(
    _: any,
    { worksheetId, cartonId, productBarcode, binNo, pickingQty },
    context: any
  ): Promise<Boolean> {
    const { domain, tx, user }: { domain: Domain; tx: EntityManager; user: User } = context.state
    try {
      console.time('ws')
      //fetch ws, wsd, oi
      const worksheet: Worksheet = await tx
        .getRepository(Worksheet)
        .createQueryBuilder('ws')
        .innerJoinAndSelect('ws.worksheetDetails', 'wsd', 'wsd.worksheet_id = ws.id')
        .innerJoinAndSelect('wsd.targetInventory', 'oi', 'oi.id= wsd.target_inventory_id')
        .where('ws.id = :worksheetId', { worksheetId: worksheetId })
        .andWhere('ws.domain_id = :domainId', { domainId: domain.id })
        .andWhere('ws.status in (:...status)', {
          status: [WORKSHEET_STATUS.EXECUTING, WORKSHEET_STATUS.PARTIAL_EXECUTING]
        })
        .andWhere('ws.type = :type', { type: WORKSHEET_TYPE.ZONE_PICKING })
        .getOne()

      if (!worksheet) throw new Error('No worksheet found')

      let worksheetDetails = worksheet.worksheetDetails
      let oiIds = worksheetDetails.map(wsd => wsd.targetInventory.id)

      // Use pessimistic lock to prevent race conditions with concurrent requests
      const targetInventories: OrderInventory[] = await tx
        .getRepository(OrderInventory)
        .createQueryBuilder('oi')
        .setLock('pessimistic_write')
        .innerJoinAndSelect('oi.inventory', 'i', 'i.id = oi.inventory_id')
        .where('oi.id IN(:...oiIds)', { oiIds: oiIds })
        .andWhere('i.carton_id = :cartonId', { cartonId: cartonId })
        .andWhere('oi.status = :status', { status: ORDER_INVENTORY_STATUS.PROCESSING })
        .orderBy('oi.created_at')
        .getMany()

      if (!targetInventories || targetInventories.length === 0) {
        throw new Error(
          `No inventory found for carton ID: ${cartonId}. Please verify the carton ID and worksheet details.`
        )
      }

      //fetch product, product detail
      const productDetail: ProductDetail = await tx
        .getRepository(ProductDetail)
        .createQueryBuilder('pd')
        .innerJoinAndSelect('pd.product', 'p')
        .innerJoin('pd.productBarcodes', 'pb')
        .where('pd.id IN (:...productDetailIds)', { productDetailIds: targetInventories.map(oi => oi.productDetailId) })
        .andWhere('p.id IN (:...productIds)', { productIds: targetInventories.map(oi => oi.productId) })
        .andWhere('pb.gtin = :productBarcode', { productBarcode: productBarcode })
        .getOne()

      if (!productDetail) {
        throw new Error(`Product not found with barcode: ${productBarcode}.`)
      }

      const product = productDetail.product
      let pickedQty: number = pickingQty ? pickingQty : 1

      // Add decimal quantity validation
      if (pickingQty % 1 !== 0 && !product.isInventoryDecimal) {
        throw new Error('Decimal quantities are not allowed for this product')
      }

      //calculate unit picked qty if user scan ctn barcode
      let matchingProduct
      let wsController = new WorksheetController(tx, domain, user)
      matchingProduct = await wsController.getDirectQty(
        {
          ...productDetail,
          product
        },
        productBarcode,
        pickedQty
      )

      if (!matchingProduct) throw new Error(wsController.ERROR_MSG.FIND.NO_RESULT(productBarcode))

      pickedQty = matchingProduct.qty

      const sumOfReleaseQty: number = targetInventories
        .filter((oi: OrderInventory) => oi.productDetailId === productDetail.id)
        .map((oi: OrderInventory) => oi.releaseQty)
        .reduce((a, b) => a + b, 0)

      // Calculate total picked quantity including the current picking
      const sumOfPickedQty: number = targetInventories
        .filter((oi: OrderInventory) => oi.productDetailId === productDetail.id)
        .map((oi: OrderInventory) => oi.pickedQty)
        .reduce((a, b) => a + b, 0)

      // Add the current picking quantity
      const totalPickedQty: number = sumOfPickedQty + pickedQty

      if (totalPickedQty > sumOfReleaseQty)
        throw new Error(
          wsController.ERROR_MSG.VALIDITY.CANT_PROCEED_STEP_BY('picking', `quantity can't exceed limitation`)
        )

      let productOrderInventory = targetInventories.filter(
        oi => oi.productId === product.id && oi.productDetailId === productDetail.id
      )

      // Track processed OI and inventory IDs to prevent duplicate processing within this transaction
      const processedOiIds: Set<string> = new Set()
      const processedInventoryDeductions: Map<string, number> = new Map()

      //update all oi
      for (const oi of productOrderInventory) {
        let targetInventory: OrderInventory = oi
        let inventory: Inventory = targetInventory.inventory
        const targetReleaseQty: number = targetInventory.releaseQty
        const targetPickedQty: number = targetInventory.pickedQty
        const releaseGood: ReleaseGood = targetInventory.releaseGood
        if (pickedQty <= 0) break

        // Skip if this OI was already processed in this transaction
        if (processedOiIds.has(targetInventory.id)) {
          console.warn(`Skipping duplicate OI processing: ${targetInventory.id}`)
          continue
        }

        // validation to prevent picking if the inventory is involved in cycle count
        let inventoryCheckItem: InventoryCheckItem = await tx.getRepository(InventoryCheckItem).findOne({
          where: {
            domain: domain,
            inventory: inventory.id,
            status: In([
              INVENTORY_CHECK_ITEM_STATUS.PENDING,
              INVENTORY_CHECK_ITEM_STATUS.INSPECTING,
              INVENTORY_CHECK_ITEM_STATUS.INSPECTED,
              INVENTORY_CHECK_ITEM_STATUS.NOT_TALLY,
              INVENTORY_CHECK_ITEM_STATUS.MISSING,
              INVENTORY_CHECK_ITEM_STATUS.ADDED
            ])
          }
        })

        if (inventoryCheckItem) {
          throw new Error(
            `Inventory currently involved in an ongoing cycle count. Please complete the cycle count to proceed with picking.`
          )
        }

        //logic for qty deduction
        if (
          targetInventory.status === ORDER_INVENTORY_STATUS.PROCESSING &&
          targetReleaseQty > targetPickedQty &&
          pickedQty > 0
        ) {
          // Re-verify OI status from database to prevent race condition
          const freshOiStatus = await tx
            .getRepository(OrderInventory)
            .createQueryBuilder('oi')
            .select('oi.status')
            .where('oi.id = :id', { id: targetInventory.id })
            .getRawOne()

          if (freshOiStatus?.oi_status !== ORDER_INVENTORY_STATUS.PROCESSING) {
            console.warn(`OI ${targetInventory.id} status changed to ${freshOiStatus?.oi_status}, skipping`)
            continue
          }

          let remainingAssignedQty: number = pickedQty - (targetReleaseQty - targetPickedQty) // -1, 0, 1
          const gapQtyRemaining: number = targetReleaseQty - targetPickedQty // > 0

          if (binNo && !targetInventory?.binLocation) {
            const binLocation: Location = await tx.getRepository(Location).findOne({
              where: { domain: domain, name: binNo, type: LOCATION_TYPE.BIN }
            })

            if (!binLocation)
              throw new Error(
                wsController.ERROR_MSG.VALIDITY.CANT_PROCEED_STEP_BY('picking', `invalid bin location id`)
              )

            targetInventory.binLocation = binLocation
          }

          targetInventory.pickedQty += pickedQty > gapQtyRemaining ? gapQtyRemaining : pickedQty
          remainingAssignedQty = remainingAssignedQty < 0 ? 0 : remainingAssignedQty

          if (targetInventory.pickedQty == targetReleaseQty) {
            //update oi and wsd to done if pickedQty matches releaseQty
            // Account for any previous deductions to this inventory in this transaction
            const previousDeduction = processedInventoryDeductions.get(inventory.id) || 0
            const adjustedInventoryQty = inventory.qty - previousDeduction
            const leftQty: number = adjustedInventoryQty - targetInventory.releaseQty

            if (leftQty < 0) {
              throw new Error(
                wsController.ERROR_MSG.VALIDITY.CANT_PROCEED_STEP_BY('picking', `quantity can't exceed limitation`)
              )
            }

            let releaseGood = await tx.getRepository(ReleaseGood).findOne({
              where: { id: targetInventory.releaseGoodId }
            })

            //update oi
            targetInventory.status = ORDER_INVENTORY_STATUS.PICKED
            await updateOrderInventories(tx, targetInventory)

            // Mark this OI as processed
            processedOiIds.add(targetInventory.id)

            //update loading wsd if exist
            let loadingWorksheetDetail: WorksheetDetail = await tx.getRepository(WorksheetDetail).findOne({
              where: { type: WORKSHEET_TYPE.LOADING, targetInventory: targetInventory }
            })

            if (loadingWorksheetDetail && loadingWorksheetDetail.status == WORKSHEET_STATUS.DEACTIVATED) {
              await tx
                .getRepository(WorksheetDetail)
                .update(
                  { id: loadingWorksheetDetail.id },
                  { status: WORKSHEET_STATUS.EXECUTING, updatedAt: new Date(), updater: user }
                )
            }

            //update inventory with proper decimal handling
            let updateInvObj = {
              qty: () => `"qty" - :deductQty::numeric`,
              lockedQty: () => `GREATEST("locked_qty" - :deductQty::numeric, 0)`,
              uomValue: () => `"uom_value" - :deductUomValue::numeric`,
              lockedUomValue: () => `GREATEST("locked_uom_value" - :deductUomValue::numeric, 0)`,
              updater: user,
              updatedAt: new Date()
            }

            await tx
              .getRepository(Inventory)
              .createQueryBuilder()
              .update(Inventory)
              .set(updateInvObj)
              .setParameter('deductQty', targetInventory.releaseQty)
              .setParameter('deductUomValue', targetInventory.releaseUomValue)
              .where('id = :id', { id: targetInventory.inventory.id })
              .returning(['qty'])
              .execute()

            // Track the deduction for this inventory
            processedInventoryDeductions.set(
              inventory.id,
              previousDeduction + targetInventory.releaseQty
            )

            console.time('inv history')
            await generateInventoryHistory(
              inventory,
              releaseGood,
              INVENTORY_TRANSACTION_TYPE.PICKING,
              -targetInventory.releaseQty,
              -targetInventory.releaseUomValue,
              user,
              tx
            )
            console.timeEnd('inv history')

            if (leftQty === 0) {
              inventory.status = INVENTORY_STATUS.TERMINATED
            }

            let worksheetDetail: WorksheetDetail = worksheetDetails.find(
              (wsd: WorksheetDetail) => wsd.targetInventory.id === targetInventory.id
            )
            await tx.getRepository(WorksheetDetail).update(
              {
                id: worksheetDetail.id
              },
              {
                status: WORKSHEET_STATUS.DONE,
                updater: user,
                updatedAt: new Date()
              }
            )
          } else {
            //updates oi if pickedQty != releaseQty
            await updateOrderInventories(tx, targetInventory)
            // Mark this OI as processed even for partial picks
            processedOiIds.add(targetInventory.id)
          }
          //deduct pickedQty for the next oi
          pickedQty = pickedQty - (targetReleaseQty - targetPickedQty)
        }
      }
      console.timeEnd('ws')
      return true
    } catch (e) {
      throw new Error(e)
    }
  },

  async completeZonePicking(_: any, { worksheetId }, context: any): Promise<Boolean> {
    return completeZonePicking(worksheetId, context, false)
  },

  async completeZonePickingPartially(_: any, { worksheetId }, context: any): Promise<Boolean> {
    return completeZonePicking(worksheetId, context, true)
  }
}

async function updateOrderInventories(tx, orderInventory) {
  //delete relations
  delete orderInventory.domainId
  delete orderInventory.bizplaceId
  delete orderInventory.creatorId
  delete orderInventory.inventoryCheckId
  delete orderInventory.inventoryId
  delete orderInventory.orderProductId
  delete orderInventory.productId
  delete orderInventory.releaseGoodId
  delete orderInventory.replenishmentId
  delete orderInventory.arrivalNoticeId
  delete orderInventory.binLocationId
  delete orderInventory.deliveryOrderId
  delete orderInventory.originLocationId
  delete orderInventory.refWorksheetId
  delete orderInventory.returnOrderId
  delete orderInventory.transferOrderId
  delete orderInventory.updaterId
  delete orderInventory.inspectedLocationId
  delete orderInventory.productDetailId
  await tx.getRepository(OrderInventory).update({ id: orderInventory.id }, orderInventory)
}

async function completeZonePicking(worksheetId: string, context: any, isPartial: boolean): Promise<Boolean> {
  const { domain, tx, user }: { domain: Domain; tx: EntityManager; user: User } = context.state

  //fetch zone picking worksheet
  const worksheet: Worksheet = await tx.getRepository(Worksheet).findOne({
    where: { id: worksheetId },
    relations: ['worksheetDetails', 'worksheetDetails.targetInventory']
  })
  const worksheetDetails: WorksheetDetail[] = worksheet.worksheetDetails
  const targetInventories: OrderInventory[] = worksheetDetails
    // .filter(wsd => wsd.status != 'MISSING' || wsd.status != 'CANCELLED')
    .map((wsd: WorksheetDetail) => wsd.targetInventory)

  let roIds = [...new Set(targetInventories.map(oi => oi.releaseGoodId))]

  //update ro status to partial processing for partial complete
  if (isPartial) {
    await tx.getRepository(ReleaseGood).update(
      { id: In(roIds), status: ORDER_STATUS.PROCESSING },
      {
        status: ORDER_STATUS.PARTIAL_PROCESSING,
        updater: user,
        updatedAt: new Date()
      }
    )
  } else if (!isPartial) {
    // When completing fully, check if all OrderInventories for each ReleaseGood are actually completed
    // Only update from PARTIAL_PROCESSING to PROCESSING if all OrderInventories are completed
    const releaseGoods: ReleaseGood[] = await tx.getRepository(ReleaseGood).find({
      where: { id: In(roIds) },
      relations: ['orderInventories']
    })

    const completedRoIds: string[] = []
    const incompleteRoIds: string[] = []

    for (const rg of releaseGoods) {
      // Check if all OrderInventories are in a completed state (not PROCESSING)
      const completedStatuses = [
        ORDER_INVENTORY_STATUS.PICKED,
        ORDER_INVENTORY_STATUS.SORTING,
        ORDER_INVENTORY_STATUS.LOADING,
        ORDER_INVENTORY_STATUS.LOADED,
        ORDER_INVENTORY_STATUS.TERMINATED,
        ORDER_INVENTORY_STATUS.MISSING,
        ORDER_INVENTORY_STATUS.CANCELLED
      ]

      const allCompleted = rg.orderInventories.every(oi => completedStatuses.includes(oi.status))

      if (allCompleted) {
        completedRoIds.push(rg.id)
      } else {
        incompleteRoIds.push(rg.id)
      }
    }

    // Update ReleaseGoods that are fully completed from PARTIAL_PROCESSING to PROCESSING
    if (completedRoIds.length > 0) {
      await tx.getRepository(ReleaseGood).update(
        { id: In(completedRoIds), status: ORDER_STATUS.PARTIAL_PROCESSING },
        {
          status: ORDER_STATUS.PROCESSING,
          updater: user,
          updatedAt: new Date()
        }
      )
    }

    // For ReleaseGoods that still have incomplete OrderInventories:
    // - If they're in PROCESSING, update to PARTIAL_PROCESSING
    // - If they're already in PARTIAL_PROCESSING, leave them as is
    if (incompleteRoIds.length > 0) {
      await tx.getRepository(ReleaseGood).update(
        { id: In(incompleteRoIds), status: ORDER_STATUS.PROCESSING },
        {
          status: ORDER_STATUS.PARTIAL_PROCESSING,
          updater: user,
          updatedAt: new Date()
        }
      )
    }
  }

  //find exist loading worksheet
  const existLoadingWorksheet: Worksheet[] = await tx
    .getRepository(Worksheet)
    .createQueryBuilder('ws')
    .innerJoinAndSelect('ws.releaseGood', 'rg', 'rg.id = ws.release_good_id')
    .where('ws.domain_id = :domainId', { domainId: domain.id })
    .andWhere('ws.type = :type', { type: WORKSHEET_TYPE.LOADING })
    .andWhere('rg.id IN (:...roIds)', { roIds })
    .getMany()

  let roWithoutLoadingWorksheet = roIds.filter(roId => !existLoadingWorksheet.some(ws => ws.releaseGood.id === roId))
  let roWithLoadingWorksheet = roIds.filter(roId => existLoadingWorksheet.some(ws => ws.releaseGood.id === roId))

  // Get disable-QC-in-loading setting
  const disableQcSetting: Setting = await tx.getRepository(Setting).findOne({
    where: {
      domain,
      name: 'disable-QC-in-loading'
    }
  })

  const disableQc = disableQcSetting?.value === 'true'

  if (roWithoutLoadingWorksheet.length > 0) {
    await generateLoadingWorksheets(tx, domain, user, roWithoutLoadingWorksheet, disableQc)
  }
  if (roWithLoadingWorksheet.length > 0) {
    if (disableQc) {
      // When QC is disabled: update PICKED to LOADING status and set sortedQty = pickedQty automatically
      // Use query builder with raw SQL to set sortedQty = pickedQty in a single update
      await tx
        .getRepository(OrderInventory)
        .createQueryBuilder()
        .update(OrderInventory)
        .set({
          status: ORDER_INVENTORY_STATUS.LOADING,
          sortedQty: () => '"picked_qty"', // Set sortedQty equal to pickedQty using raw SQL expression
          updater: user,
          updatedAt: new Date()
        })
        .where('release_good_id IN (:...roIds)', { roIds: roWithLoadingWorksheet })
        .andWhere('status = :status', { status: ORDER_INVENTORY_STATUS.PICKED })
        .execute()

      const loadingOrderInventories: OrderInventory[] = await tx.getRepository(OrderInventory).find({
        where: { releaseGood: In(roWithLoadingWorksheet), status: ORDER_INVENTORY_STATUS.LOADING }
      })

      //update loading wsd deactivated to executing
      await tx.getRepository(WorksheetDetail).update(
        {
          targetInventory: In(loadingOrderInventories.map(oi => oi.id)),
          type: WORKSHEET_TYPE.LOADING,
          status: WORKSHEET_STATUS.DEACTIVATED
        },
        { status: WORKSHEET_STATUS.EXECUTING, updatedAt: new Date(), updater: user }
      )
    } else {
      // When QC is enabled: update PICKED to SORTING status (existing behavior)
      await tx.getRepository(OrderInventory).update(
        { releaseGood: In(roWithLoadingWorksheet), status: ORDER_INVENTORY_STATUS.PICKED },
        {
          status: ORDER_INVENTORY_STATUS.SORTING,
          updater: user,
          updatedAt: new Date()
        }
      )

      const loadingOrderInventories: OrderInventory[] = await tx.getRepository(OrderInventory).find({
        where: { releaseGood: In(roWithLoadingWorksheet), status: ORDER_INVENTORY_STATUS.SORTING }
      })

      //update loading wsd deactivated to executing
      await tx.getRepository(WorksheetDetail).update(
        {
          targetInventory: In(loadingOrderInventories.map(oi => oi.id)),
          type: WORKSHEET_TYPE.LOADING,
          status: WORKSHEET_STATUS.DEACTIVATED
        },
        { status: WORKSHEET_STATUS.EXECUTING, updatedAt: new Date(), updater: user }
      )
    }
  }

  const wsStatus = isPartial ? WORKSHEET_STATUS.PARTIAL_EXECUTING : WORKSHEET_STATUS.DONE
  await updateWorksheetsAndDetails(tx, worksheet, worksheetDetails, wsStatus, user)

  return true
}

async function generateLoadingWorksheets(
  tx: EntityManager,
  domain: Domain,
  user: User,
  roWithoutLoadingWorksheet,
  disableQc: boolean
) {
  const releaseGoods: ReleaseGood[] = await tx.getRepository(ReleaseGood).find({
    where: { id: In(roWithoutLoadingWorksheet), status: Not(In(ORDER_STATUS.OBSOLETE)) },
    relations: ['orderInventories', 'sortedBy']
  })

  for (const ro of releaseGoods) {
    const loadingWorksheet = {
      name: `LOAD-` + uuidv4(),
      type: WORKSHEET_TYPE.LOADING,
      status: WORKSHEET_STATUS.EXECUTING,
      startedAt: new Date(),
      createdAt: new Date(),
      domain: ro.domainId,
      bizplace: ro.bizplaceId,
      releaseGood: ro,
      creator: user,
      sorter: ro.sortedBy ? ro.sortedBy : null
    }

    await tx.getRepository(Worksheet).insert(loadingWorksheet)

    const orderInventories = ro.orderInventories
    const loadingWorksheetDetails = orderInventories.map(oi => {
      const status =
        oi.status === ORDER_INVENTORY_STATUS.PICKED ? WORKSHEET_STATUS.EXECUTING : WORKSHEET_STATUS.DEACTIVATED
      return {
        name: `LOAD-DETAIL-` + uuidv4(),
        type: WORKSHEET_TYPE.LOADING,
        status: status,
        createdAt: new Date(),
        domain: domain,
        bizplace: ro.bizplaceId,
        worksheet: loadingWorksheet,
        targetInventory: oi,
        creator: user
      }
    })

    await tx.getRepository(WorksheetDetail).insert(loadingWorksheetDetails)

    // Update picked OrderInventories based on disableQc setting
    if (disableQc) {
      // When QC is disabled: update PICKED to LOADING status and set sortedQty = pickedQty automatically
      await tx
        .getRepository(OrderInventory)
        .createQueryBuilder()
        .update(OrderInventory)
        .set({
          status: ORDER_INVENTORY_STATUS.LOADING,
          sortedQty: () => '"picked_qty"', // Set sortedQty equal to pickedQty using raw SQL expression
          updater: user,
          updatedAt: new Date()
        })
        .where('id IN (:...oiIds)', { oiIds: orderInventories.map(oi => oi.id) })
        .andWhere('status = :status', { status: ORDER_INVENTORY_STATUS.PICKED })
        .execute()
    } else {
      // When QC is enabled: update PICKED to SORTING status (existing behavior)
      await tx.getRepository(OrderInventory).update(
        { id: In(orderInventories.map(oi => oi.id)), status: ORDER_INVENTORY_STATUS.PICKED },
        {
          status: ORDER_INVENTORY_STATUS.SORTING,
          updater: user,
          updatedAt: new Date()
        }
      )
    }
  }
}

async function updateWorksheetsAndDetails(
  tx: EntityManager,
  worksheet: Worksheet,
  worksheetDetails: WorksheetDetail,
  status,
  updater: User
) {
  worksheet.status = status
  worksheet.endedAt = status === WORKSHEET_STATUS.DONE ? new Date() : null
  worksheet.updater = updater
  worksheet.updatedAt = new Date()

  // Filter only the worksheet details that need to be updated
  const updateWorksheetDetails = worksheetDetails.filter(
    wsd => wsd.targetInventory?.status === ORDER_INVENTORY_STATUS.PICKED
  )

  if (updateWorksheetDetails.length > 0) {
    await tx.getRepository(WorksheetDetail).update(
      { id: In(updateWorksheetDetails.map(wsd => wsd.id)) },
      {
        status: WORKSHEET_STATUS.DONE,
        updater: updater
      }
    )
  }

  await tx.getRepository(Worksheet).save(worksheet)
}
