import { EntityManager, In, Not } from 'typeorm'

import { User } from '@things-factory/auth-base'
import { Bizplace } from '@things-factory/biz-base'
import { Product, ProductDetail } from '@things-factory/product-base'
import {
  ORDER_INVENTORY_STATUS,
  ORDER_PRODUCT_STATUS,
  OrderInventory,
  OrderNoGenerator,
  OrderProduct,
  ReleaseGood
} from '@things-factory/sales-base'
import { Domain } from '@things-factory/shell'
import { Inventory } from '@things-factory/warehouse-base'
import {
  generatePickingWorksheetDetail,
  Worksheet,
  WORKSHEET_STATUS,
  WORKSHEET_TYPE,
  WorksheetDetail
} from '@things-factory/worksheet-base'
import { getTmsService } from '@things-factory/integration-lmd'

export const addReleaseGoodProducts = {
  async addReleaseGoodProducts(_: any, { name, orderInventories, existingOrderInventories }, context: any) {
    try {
      const { tx, user, domain }: { tx: EntityManager; user: User; domain: Domain } = context.state

      let releaseGood: ReleaseGood = await tx.getRepository(ReleaseGood).findOne({
        where: { domain, name: name },
        relations: ['bizplace', 'orderProducts', 'lastMileDelivery']
      })

      const bizplace: Bizplace = releaseGood.bizplace

      let pickingWorksheet: Worksheet = await tx.getRepository(Worksheet).findOne({
        where: { releaseGood, type: WORKSHEET_TYPE.PICKING }
      })

      let loadingWorksheet: Worksheet = await tx.getRepository(Worksheet).findOne({
        where: { releaseGood, type: WORKSHEET_TYPE.LOADING }
      })

      if (existingOrderInventories) {
        for (let oi of existingOrderInventories) {
          // map input to OrderInventory Object
          let curOrderInv: OrderInventory = Object.assign({}, oi)
          curOrderInv.domain = domain
          curOrderInv.bizplace = bizplace
          curOrderInv.releaseGood = releaseGood
          curOrderInv.productDetail = await tx.getRepository(ProductDetail).findOne(oi.productDetail.id)

          let existingOrderInv: OrderInventory

          if (curOrderInv?.inventory?.id) {
            let foundInv: Inventory = await tx.getRepository(Inventory).findOne(curOrderInv.inventory.id)
            curOrderInv.inventory = foundInv

            existingOrderInv = await tx.getRepository(OrderInventory).findOne({
              where: {
                releaseGood,
                productDetail: curOrderInv.productDetail,
                batchId: curOrderInv.batchId,
                inventory: foundInv
              }
            })

            if (foundInv && foundInv.lockedQty == 0) {
              foundInv.qty += Number(existingOrderInv.releaseQty)
              foundInv.uomValue += Number(existingOrderInv.releaseUomValue)
              foundInv.lockedQty = Number(existingOrderInv.releaseQty)
              foundInv.lockedUomValue = Number(existingOrderInv.releaseUomValue)
            }

            if (curOrderInv.releaseQty < existingOrderInv.pickedQty) {
              throw new Error(`${curOrderInv.inventory.palletId} has already picked ${existingOrderInv.pickedQty}`)
            }

            if (curOrderInv.releaseQty === 0) {
              foundInv.lockedQty = Number(foundInv.lockedQty) - Number(existingOrderInv.releaseQty)
              foundInv.lockedUomValue = Number(foundInv.lockedUomValue) - Number(existingOrderInv.releaseUomValue)

              await tx.getRepository(Inventory).save(foundInv)

              if (existingOrderInv) {
                // if found existing OrderInventory and worksheetDetail then qty, uomValue, and status are updated
                curOrderInv = {
                  ...existingOrderInv,
                  releaseQty: curOrderInv.releaseQty,
                  releaseUomValue: curOrderInv.releaseUomValue,
                  lockedUomValue: curOrderInv.lockedUomValue,
                  status: ORDER_INVENTORY_STATUS.CANCELLED
                }

                let existingWorksheetDetail: WorksheetDetail = await tx.getRepository(WorksheetDetail).findOne({
                  where: {
                    worksheet: pickingWorksheet,
                    type: WORKSHEET_TYPE.PICKING,
                    targetInventory: curOrderInv
                  }
                })

                if (existingWorksheetDetail) {
                  existingWorksheetDetail = {
                    ...existingWorksheetDetail,
                    status: WORKSHEET_STATUS.CANCELLED
                  }
                  await tx.getRepository(WorksheetDetail).save(existingWorksheetDetail)
                }

                await tx.getRepository(OrderInventory).save(curOrderInv)
              }
            } else if (curOrderInv.releaseQty == existingOrderInv.pickedQty) {
              curOrderInv = {
                ...existingOrderInv,
                releaseQty: curOrderInv.releaseQty,
                releaseUomValue: curOrderInv.releaseUomValue,
                lockedUomValue: curOrderInv.lockedUomValue,
                status: ORDER_INVENTORY_STATUS.PICKED
              }

              let existingWorksheetDetail: WorksheetDetail = await tx.getRepository(WorksheetDetail).findOne({
                where: {
                  targetInventory: curOrderInv
                }
              })

              existingWorksheetDetail = {
                ...existingWorksheetDetail,
                status: WORKSHEET_STATUS.PICKED
              }

              await tx.getRepository(WorksheetDetail).save(existingWorksheetDetail)
              await tx.getRepository(OrderInventory).save(curOrderInv)
            } else {
              const qtyDelta = Number(curOrderInv.releaseQty) - Number(existingOrderInv.releaseQty)
              const uomDelta = Number(curOrderInv.releaseUomValue) - Number(existingOrderInv.releaseUomValue)

              const updateResult = await tx
                .getRepository(Inventory)
                .createQueryBuilder()
                .update(Inventory)
                .set({
                  lockedQty: () => `GREATEST(COALESCE("locked_qty", 0) + :qtyDelta::numeric, 0)`,
                  lockedUomValue: () => `GREATEST(COALESCE("locked_uom_value", 0) + :uomDelta::numeric, 0)`,
                  updater: user
                })
                .setParameter('qtyDelta', qtyDelta)
                .setParameter('uomDelta', uomDelta)
                .where(
                  qtyDelta > 0
                    ? 'id = :id AND qty >= COALESCE(locked_qty, 0) + :newQty'
                    : 'id = :id',
                  {
                    id: foundInv.id,
                    ...(qtyDelta > 0 ? { newQty: qtyDelta } : {})
                  }
                )
                .execute()

              if (updateResult.affected === 0) {
                throw new Error(`Insufficient inventory for pallet ${foundInv.palletId}`)
              }
              foundInv = await tx.getRepository(Inventory).findOne(foundInv.id)

              if (existingOrderInv) {
                // if found existing OrderInventory and worksheetDetail then qty, uomValue, and status are updated
                curOrderInv = {
                  ...existingOrderInv,
                  releaseQty: curOrderInv.releaseQty,
                  releaseUomValue: curOrderInv.releaseUomValue,
                  status: pickingWorksheet ? ORDER_INVENTORY_STATUS.PICKING : ORDER_INVENTORY_STATUS.PENDING
                }

                let existingWorksheetDetail: WorksheetDetail = await tx.getRepository(WorksheetDetail).findOne({
                  where: {
                    worksheet: pickingWorksheet,
                    type: WORKSHEET_TYPE.PICKING,
                    targetInventory: existingOrderInv
                  }
                })

                if (existingWorksheetDetail) {
                  if (curOrderInv.releaseQty != existingOrderInv.releaseQty) {
                    existingWorksheetDetail = {
                      ...existingWorksheetDetail,
                      status: WORKSHEET_STATUS.DEACTIVATED
                    }

                    await tx.getRepository(WorksheetDetail).save(existingWorksheetDetail)
                  }
                }

                await tx.getRepository(OrderInventory).save(curOrderInv)
              }
            }

            if (pickingWorksheet) {
              // if has picking worksheet, status will be deactivated to prevent any action from warehouse operator until office admin has confirmation
              pickingWorksheet = {
                ...pickingWorksheet,
                status: WORKSHEET_STATUS.DEACTIVATED
              }
              await tx.getRepository(Worksheet).save(pickingWorksheet)
            }

            if (loadingWorksheet) {
              // if has loading worksheet, status will be deactivated to prevent any action from warehouse operator until office admin has confirmation
              loadingWorksheet = {
                ...loadingWorksheet,
                status: WORKSHEET_STATUS.DEACTIVATED
              }

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

      if (orderInventories.length > 0) {
        for (let oi of orderInventories) {
          // map input to OrderInventory Object
          let newOrderInv: OrderInventory = Object.assign({}, oi)
          newOrderInv.domain = domain
          newOrderInv.bizplace = bizplace
          newOrderInv.batchId = oi.batchId
          newOrderInv.packingType = oi.packingType
          newOrderInv.status = pickingWorksheet ? ORDER_INVENTORY_STATUS.PICKING : ORDER_INVENTORY_STATUS.PENDING
          newOrderInv.name = OrderNoGenerator.orderInventory()
          newOrderInv.releaseGood = releaseGood
          newOrderInv.creator = user
          newOrderInv.updater = user

          let existingOrderInv: OrderInventory

          let existingOrderProduct: OrderProduct = await tx.getRepository(OrderProduct).findOne({
            where: {
              domain,
              releaseGood,
              batchId: oi.batchId,
              productDetail: newOrderInv.productDetail
            }
          })

          // check if it is release by inventory (has inventory value) or product
          if (newOrderInv.inventory?.id) {
            // if release by inventory, then quantity and uomValue values are updated
            let foundInv: Inventory = await tx
              .getRepository(Inventory)
              .findOne({ where: { id: newOrderInv.inventory.id }, relations: ['productDetail'] })
            newOrderInv.inventory = foundInv
            newOrderInv.uom = oi?.uom || foundInv.uom
            newOrderInv.batchIdRef = oi?.batchIdRef || foundInv.batchIdRef

            if (foundInv.productDetail) {
              newOrderInv.uomValue = foundInv.productDetail.uomValue
              delete foundInv.productDetail
            }

            // check for existing released OrderInventory specifying product, batchId, packingType, and inventory
            existingOrderInv = await tx.getRepository(OrderInventory).findOne({
              where: {
                releaseGood,
                productDetail: newOrderInv.productDetail,
                batchId: newOrderInv.batchId,
                inventory: foundInv
              }
            })

            const lockResult = await tx
              .getRepository(Inventory)
              .createQueryBuilder()
              .update(Inventory)
              .set({
                lockedQty: () => `COALESCE("locked_qty", 0) + :releaseQty::numeric`,
                lockedUomValue: () => `COALESCE("locked_uom_value", 0) + :releaseUomValue::numeric`,
                updater: user
              })
              .setParameter('releaseQty', newOrderInv.releaseQty)
              .setParameter('releaseUomValue', newOrderInv.releaseUomValue)
              .where('id = :id AND qty >= COALESCE(locked_qty, 0) + :newQty', {
                id: foundInv.id,
                newQty: newOrderInv.releaseQty
              })
              .execute()

            if (lockResult.affected === 0) {
              throw new Error(`Insufficient inventory for pallet ${foundInv.palletId}`)
            }
            // Re-read foundInv for downstream use
            foundInv = await tx.getRepository(Inventory).findOne(foundInv.id)

            let newOrderProduct: OrderProduct = Object.assign(new OrderProduct(), newOrderInv)
            newOrderProduct = {
              ...newOrderProduct,
              packQty: 0,
              actualPackQty: 0,
              palletQty: 0,
              actualPalletQty: 0,
              status: ORDER_PRODUCT_STATUS.ASSIGNED
            }

            if (!existingOrderProduct) newOrderProduct = await tx.getRepository(OrderProduct).save(newOrderProduct)

            newOrderInv.orderProduct = existingOrderProduct ? existingOrderProduct : newOrderProduct
          } else {
            // check for existing released OrderInventory specifying product, batchId and packingType
            existingOrderInv = await tx.getRepository(OrderInventory).findOne({
              where: {
                releaseGood,
                productDetail: newOrderInv.productDetail,
                batchId: newOrderInv.batchId
              }
            })
          }

          if (existingOrderInv) {
            // if found existing OrderInventory and worksheetDetail then qty, uomValue, and status are updated
            newOrderInv = {
              ...existingOrderInv,
              releaseQty:
                existingOrderInv.status === ORDER_INVENTORY_STATUS.REPLACED
                  ? newOrderInv.releaseQty
                  : existingOrderInv.releaseQty + newOrderInv.releaseQty,
              releaseUomValue: ORDER_INVENTORY_STATUS.REPLACED
                ? newOrderInv.releaseUomValue
                : existingOrderInv.releaseUomValue + newOrderInv.releaseUomValue,
              status:
                existingOrderInv.status === ORDER_INVENTORY_STATUS.CANCELLED
                  ? pickingWorksheet
                    ? ORDER_INVENTORY_STATUS.PICKING
                    : ORDER_INVENTORY_STATUS.PENDING
                  : existingOrderInv.status
            }

            let existingWorksheetDetail: WorksheetDetail = await tx.getRepository(WorksheetDetail).findOne({
              where: {
                worksheet: pickingWorksheet,
                type: WORKSHEET_TYPE.PICKING,
                targetInventory: existingOrderInv
              }
            })

            if (existingWorksheetDetail) {
              existingWorksheetDetail = {
                ...existingWorksheetDetail,
                targetProduct: newOrderInv.orderProduct,
                status: WORKSHEET_STATUS.DEACTIVATED
              }

              await tx.getRepository(WorksheetDetail).save(existingWorksheetDetail)
            }
          }

          let mappedOrderInv = data => {
            let mapOrdInv = Object.assign(new OrderInventory(), {
              name: data.name,
              type: data.type,
              releaseQty: data.releaseQty,
              releaseUomValue: data.releaseUomValue,
              batchId: data.batchId,
              batchIdRef: data.batchIdRef,
              packingType: data.packingType,
              uom: data.uom,
              product: data.product,
              productDetail: data.productDetail,
              inventory: data.inventory,
              status: data.status,
              releaseGood: data.releaseGood,
              orderProduct: data.orderProduct,
              bizplace: data.bizplace,
              domain: domain,
              creator: user,
              updater: user
            })
            return mapOrdInv
          }

          if (releaseGood.status === 'PENDING') {
            let savedOrderInv = mappedOrderInv(newOrderInv)

            savedOrderInv = await tx.getRepository(OrderInventory).save(savedOrderInv)
          }

          if (pickingWorksheet) {
            let savedOrderInv = mappedOrderInv(newOrderInv)

            if (newOrderInv?.id) {
              // newOrderInv will have id if it was updated from existingOrderInv
              // this solution will remove ix_order-inventory_0 unique error
              // ** please revise this resolver in future. looks like there's few
              //    things that can refactor
              savedOrderInv.id = newOrderInv.id
            }

            savedOrderInv = await tx.getRepository(OrderInventory).save(savedOrderInv)

            if (!existingOrderInv && pickingWorksheet) {
              // if this is a new orderInventory and has existing worksheet then generate a new worksheet detail for it
              await generatePickingWorksheetDetail(tx, domain, user, pickingWorksheet, savedOrderInv)
            }

            // if has picking worksheet, status will be deactivated to prevent any action from warehouse operator until office admin has confirmation
            await tx.getRepository(Worksheet).update(pickingWorksheet.id, { status: WORKSHEET_STATUS.DEACTIVATED })
          }
        }
      }

      if (loadingWorksheet) {
        // if has loading worksheet, status will be deactivated to prevent any action from warehouse operator until office admin has confirmation
        loadingWorksheet = {
          ...loadingWorksheet,
          status: WORKSHEET_STATUS.DEACTIVATED
        }

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

      // Update Order Product Release Qty
      let rgOrderProducts: OrderProduct[] = await tx
        .getRepository(OrderProduct)
        .find({ where: { domain, releaseGood } })

      await Promise.all(
        rgOrderProducts.map(async op => {
          const orderInventories: OrderInventory[] = await tx.getRepository(OrderInventory).find({
            where: {
              domain,
              releaseGood,
              orderProduct: op,
              status: Not(
                In([
                  ORDER_INVENTORY_STATUS.TERMINATED,
                  ORDER_INVENTORY_STATUS.CANCELLED,
                  ORDER_INVENTORY_STATUS.PENDING_CANCEL
                ])
              )
            }
          })

          let totalReleaseQty: number = orderInventories.reduce((total, currentValue) => {
            total += currentValue.releaseQty
            return total
          }, 0)

          let totalReleaseUomValue: number = orderInventories.reduce((total, currentValue) => {
            total += currentValue.releaseUomValue
            return total
          }, 0)

          let updateOPInfo: any = {
            releaseQty: totalReleaseQty,
            releaseUomValue: totalReleaseUomValue
          }

          if (totalReleaseQty <= 0) {
            updateOPInfo.status = ORDER_PRODUCT_STATUS.TERMINATED
          }

          await tx.getRepository(OrderProduct).update({ id: op.id }, updateOPInfo)
        })
      )

      if (releaseGood?.lastMileDelivery && releaseGood?.lastMileDelivery?.isTms) {
        // if releaseGood has last mile delivery, then update the last mile delivery with new order inventories
        const tmsService = getTmsService(releaseGood.lastMileDelivery.platform)
        tmsService.updateShipment(releaseGood)
      }
    } catch (error) {
      throw error
    }
  }
}
