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

import { User } from '@things-factory/auth-base'
import { Domain } from '@things-factory/shell'
import {
  ORDER_INVENTORY_STATUS,
  OrderInventory,
  OrderNoGenerator,
  OrderTote,
  OrderToteItem,
  ReleaseGood
} from '@things-factory/sales-base'
import { Tote, TOTE_STATUS } from '@things-factory/warehouse-base'

/**
 * Calculates the available quantity that can still be sorted for an order inventory
 */
function getAvailableQty(oi: OrderInventory): number {
  const available = Math.min(oi.releaseQty, oi.pickedQty) - oi.sortedQty
  return available > 0 ? available : 0
}

/**
 * Creates or updates an order tote item with the given quantity
 */
async function upsertOrderToteItem(
  tx: EntityManager,
  domain: Domain,
  user: User,
  orderTote: OrderTote,
  orderInventory: OrderInventory,
  qtyToAdd: number
): Promise<void> {
  const orderToteItemRepo = tx.getRepository(OrderToteItem)
  const existingItem = (await orderToteItemRepo.findOne({
    where: {
      domain,
      orderTote,
      orderInventory,
      orderProduct: orderInventory.orderProduct
    }
  })) as OrderToteItem | null

  if (!existingItem) {
    await orderToteItemRepo.save({
      domain,
      name: OrderNoGenerator.orderToteItem(),
      orderTote,
      orderInventory,
      orderProduct: orderInventory.orderProduct,
      qty: qtyToAdd,
      updater: user
    })
  } else {
    await orderToteItemRepo.update(existingItem.id, {
      qty: Number(existingItem.qty ?? 0) + qtyToAdd,
      updater: user,
      updatedAt: new Date()
    } as any)
  }
}

export const sortItem = {
  async sortItem(_: any, { releaseGoodNo, productBarcode, qty, worksheetDetails, toteNo }, context: any) {
    const { domain, user, tx }: { domain: Domain; user: User; tx: EntityManager } = context.state

    // Single query to get order inventories based on either releaseGood name or binLocation
    const orderInventories: OrderInventory[] = await tx
      .getRepository(OrderInventory)
      .createQueryBuilder('orderInventory')
      .innerJoinAndSelect('orderInventory.productDetail', 'productDetail')
      .innerJoinAndSelect('productDetail.productBarcodes', 'productBarcode')
      .leftJoinAndSelect('orderInventory.orderProduct', 'orderProduct')
      .leftJoinAndSelect('orderInventory.releaseGood', 'releaseGood')
      .leftJoinAndSelect('releaseGood.bizplace', 'bizplace')
      .leftJoinAndSelect('orderInventory.binLocation', 'binLocation')
      .where('orderInventory.domain_id = :domainId', { domainId: domain.id })
      .andWhere('orderInventory.status IN (:...statuses)', {
        statuses: [ORDER_INVENTORY_STATUS.SORTING, ORDER_INVENTORY_STATUS.LOADING]
      })
      .andWhere('productBarcode.gtin = :gtin', { gtin: productBarcode })
      .andWhere(
        new Brackets(qb => {
          qb.where('orderInventory.sortedQty < orderInventory.pickedQty').andWhere(
            'orderInventory.sortedQty < orderInventory.releaseQty'
          )
        })
      )
      .andWhere(
        new Brackets(qb => {
          qb.where('releaseGood.name = :releaseGoodNo', { releaseGoodNo }).orWhere(
            'binLocation.name = :releaseGoodNo',
            { releaseGoodNo }
          )
        })
      )
      .getMany()

    if (!orderInventories.length) throw new Error('No matching product barcode found')

    let resolvedReleaseGood: ReleaseGood | null = null
    let resolvedOrderTote: OrderTote | null = null

    if (toteNo) {
      // Ensure we can identify a single release order when scanning by BIN
      const uniqueReleaseGoodIds = [...new Set(orderInventories.map(oi => oi.releaseGoodId).filter(Boolean))]
      if (uniqueReleaseGoodIds.length > 1) {
        throw new Error('Multiple release orders detected. Please select a release order before tote scanning.')
      }

      resolvedReleaseGood = await tx.getRepository(ReleaseGood).findOne({
        where: { domain, id: uniqueReleaseGoodIds[0] },
        relations: ['bizplace']
      })

      if (!resolvedReleaseGood) {
        throw new Error('Release good not found for tote scanning')
      }

      resolvedOrderTote = await tx.getRepository(OrderTote).findOne({
        where: { domain, name: toteNo, releaseGood: resolvedReleaseGood }
      })

      if (!resolvedOrderTote) {
        // Query for valid totes (NOT dispatched or damaged)
        const foundTote: Tote = await tx.getRepository(Tote).findOne({
          where: {
            bizplace: resolvedReleaseGood.bizplace,
            name: toteNo,
            deletedAt: IsNull(),
            status: Not(In([TOTE_STATUS.DISPATCHED, TOTE_STATUS.DAMAGED]))
          }
        })

        resolvedOrderTote = await tx.getRepository(OrderTote).save({
          name: toteNo,
          domain,
          releaseGood: resolvedReleaseGood,
          tote: foundTote ?? null,
          creator: user,
          updater: user
        })
      } else if (resolvedOrderTote.closedDate) {
        throw new Error('Tote has been sealed, please try another tote!')
      }
    }

    // Calculate total available quantity using helper function
    const totalAvailableQty = orderInventories.reduce((sum, oi) => sum + getAvailableQty(oi), 0)

    if (qty > totalAvailableQty) {
      throw new Error(`Sort qty (${qty}) exceeds total available quantity (${totalAvailableQty})`)
    }

    // Distribute the sorted quantity across order inventories
    let remainingQty = qty
    for (const orderInventory of orderInventories) {
      if (remainingQty <= 0) break

      const availableQty = getAvailableQty(orderInventory)
      if (availableQty <= 0) continue

      const qtyToAdd = Math.min(remainingQty, availableQty)

      // Update the order inventory
      await tx.getRepository(OrderInventory).update(orderInventory.id, {
        sortedQty: orderInventory.sortedQty + qtyToAdd,
        status: ORDER_INVENTORY_STATUS.LOADING,
        updatedAt: new Date(),
        updater: user
      })

      // Create / update order tote items to reflect tote allocation during QC
      if (resolvedOrderTote) {
        await upsertOrderToteItem(tx, domain, user, resolvedOrderTote, orderInventory, qtyToAdd)
      }

      remainingQty -= qtyToAdd
    }

    return true
  }
}
