import { EntityManager } from 'typeorm'

import { Location } from '@things-factory/warehouse-base'
import { Domain } from '@things-factory/shell'
import { OrderInventory, ORDER_INVENTORY_STATUS, ORDER_STATUS, ReleaseGood } from '@things-factory/sales-base'
import { ProductDetail } from '@things-factory/product-base'
import { computeConversionString } from '@things-factory/worksheet-base'

/**
 * Custom error class for outbound order related errors
 * Covers both ReleaseGood and OrderInventory related errors
 */
class OutboundOrderError extends Error {
  constructor(message: string, public code: string) {
    super(message)
    this.name = 'OutboundOrderError'
  }
}

/**
 * Builds a query to find a release good by order number
 */
const buildfindReleaseGoodByNameQuery = (tx: EntityManager, domainId: string, orderNo: string) => {
  return tx
    .getRepository(ReleaseGood)
    .createQueryBuilder('releaseGood')
    .leftJoinAndSelect('releaseGood.bizplace', 'bizplace')
    .where('releaseGood.domain_id = :domainId', { domainId })
    .andWhere('releaseGood.name = :orderNo', { orderNo })
    .andWhere('releaseGood.status IN (:...status)', {
      status: [ORDER_STATUS.LOADING, ORDER_STATUS.PROCESSING, ORDER_STATUS.PARTIAL_PROCESSING, ORDER_STATUS.DONE]
    })
}

/**
 * Builds a query to find order inventories
 * @param tx - Entity manager
 * @param domainId - Domain ID
 * @param options - Optional filters: locationId for bin location, releaseGoodId for release good
 */
const buildOrderInventoryQuery = (
  tx: EntityManager,
  domainId: string,
  options?: { locationId?: string; releaseGoodId?: string }
) => {
  const queryBuilder = tx
    .getRepository(OrderInventory)
    .createQueryBuilder('orderInventory')
    .leftJoinAndSelect('orderInventory.inventory', 'inventory')
    .leftJoinAndSelect('inventory.product', 'product')
    .leftJoinAndSelect('inventory.productDetail', 'productDetail')
    .leftJoinAndSelect('productDetail.productBarcodes', 'productBarcodes')
    .leftJoinAndSelect('productDetail.childProductDetail', 'childProductDetail')
    .leftJoinAndSelect('productDetail.parentProductDetails', 'parentProductDetails')
    .leftJoinAndSelect('orderInventory.binLocation', 'binLocation')
    .innerJoinAndSelect('orderInventory.releaseGood', 'releaseGood')
    .where('orderInventory.domain_id = :domainId', { domainId })
    .andWhere('orderInventory.status IN (:...orderInventoryStatus)', {
      orderInventoryStatus: [
        ORDER_INVENTORY_STATUS.SORTING,
        ORDER_INVENTORY_STATUS.LOADING,
        ORDER_INVENTORY_STATUS.LOADED
      ]
    })

  if (options?.locationId) {
    queryBuilder.andWhere('orderInventory.bin_location_id = :locationId', { locationId: options.locationId })
  }

  if (options?.releaseGoodId) {
    queryBuilder.andWhere('orderInventory.release_good_id = :releaseGoodId', { releaseGoodId: options.releaseGoodId })
  }

  return queryBuilder.orderBy('orderInventory.createdAt', 'ASC')
}

/**
 * Transforms order inventory data into the required worksheet format
 * @param orderInventories - Array of order inventories
 * @param releaseGood - Optional release good (when scanning by order number)
 */
const transformToWorksheetFormat = (orderInventories: OrderInventory[], releaseGood?: ReleaseGood) => {
  if (!orderInventories.length) return null

  // Extract unique bin location names
  const uniqueBinLocations = [...new Set(orderInventories.map(oi => oi.binLocation?.name).filter(Boolean))]

  // Get release good from first order inventory if not provided
  const firstReleaseGood = releaseGood || orderInventories[0]?.releaseGood

  return {
    worksheetInfo: {
      worksheet: {
        id: null,
        name: null,
        type: null,
        status: null
      },
      bizplaceName: null,
      startedAt: null,
      releaseGood: firstReleaseGood
        ? {
            id: firstReleaseGood.id,
            name: firstReleaseGood.name,
            attentionCompany: firstReleaseGood.attentionCompany,
            attentionTo: firstReleaseGood.attentionTo,
            deliveryAddress1: firstReleaseGood.deliveryAddress1,
            deliveryAddress2: firstReleaseGood.deliveryAddress2,
            status: firstReleaseGood.status,
            refNo: firstReleaseGood.refNo,
            refNo2: firstReleaseGood.refNo2,
            refNo3: firstReleaseGood.refNo3,
            rtm: firstReleaseGood.rtm
          }
        : null,
      binLocationName: uniqueBinLocations.join(', '), // Join multiple bin locations with comma
      isReleaseGoodScan: !!releaseGood // Flag to indicate if this result came from scanning by release good
    },
    worksheetDetailInfos: orderInventories.map(oi => ({
      id: null,
      name: null,
      batchId: oi.batchId,
      releaseQty: oi.releaseQty,
      pickedQty: oi.pickedQty,
      sortedQty: oi.sortedQty,
      status: oi.status,
      packingType: oi.packingType,
      packingSize: oi.packingSize,
      expirationDate: oi.inventory?.expirationDate,
      binLocation: {
        name: oi.binLocation?.name
      },
      // Include releaseGood for each item to support filtering by release order in bin scan mode
      releaseGood: oi.releaseGood
        ? {
            id: oi.releaseGood.id,
            name: oi.releaseGood.name,
            attentionCompany: oi.releaseGood.attentionCompany,
            attentionTo: oi.releaseGood.attentionTo,
            deliveryAddress1: oi.releaseGood.deliveryAddress1,
            deliveryAddress2: oi.releaseGood.deliveryAddress2,
            status: oi.releaseGood.status,
            refNo: oi.releaseGood.refNo,
            refNo2: oi.releaseGood.refNo2,
            refNo3: oi.releaseGood.refNo3
          }
        : null,
      productName: oi.inventory?.product?.name,
      productSku: oi.inventory?.product?.sku,
      conversion: computeConversionString(oi.releaseQty, oi.inventory?.productDetail as ProductDetail),
      relatedOrderInv: {
        id: oi.id,
        batchId: oi.batchId,
        packingType: oi.packingType,
        refWorksheetId: oi.refWorksheetId,
        packedQty: oi.packedQty,
        releaseQty: oi.releaseQty,
        product: oi.inventory?.product
          ? {
              id: oi.inventory.product.id,
              sku: oi.inventory.product.sku,
              name: oi.inventory.product.name,
              isInventoryDecimal: oi.inventory.product.isInventoryDecimal
            }
          : null,
        productDetail: oi.inventory?.productDetail
          ? {
              id: oi.inventory.productDetail.id,
              productBarcodes: (oi.inventory.productDetail.productBarcodes || []).map((pb: { gtin: string }) => ({ gtin: pb.gtin }))
            }
          : null,
        orderProduct: oi.orderProductId
          ? {
              id: oi.orderProductId
            }
          : null
      }
    }))
  }
}

/**
 * Finds loadable release goods by bin location
 * @param _ - GraphQL resolver parent object
 * @param orderNo - The order number or bin location name to search for
 * @param context - GraphQL context containing domain and transaction manager
 * @returns Promise<Object> - Transformed worksheet data structure
 * @throws OutboundOrderError if order is DONE or if no items found in bin
 */
export const findLoadableReleaseGoodByBin = {
  async findLoadableReleaseGoodByBin(_: any, { orderNo }: { orderNo: string }, context: any): Promise<any> {
    const { domain, tx }: { domain: Domain; tx: EntityManager } = context.state

    let orderInventoryByBin: OrderInventory[] = []

    let foundReleaseGood: ReleaseGood = await buildfindReleaseGoodByNameQuery(tx, domain.id, orderNo).getOne()

    if (foundReleaseGood && foundReleaseGood.status == ORDER_STATUS.DONE) {
      throw new OutboundOrderError('This order is DONE', 'ORDER_DONE')
    } else if (foundReleaseGood) {
      // Found release good by order number - query its order inventories and return worksheet format
      orderInventoryByBin = await buildOrderInventoryQuery(tx, domain.id, {
        releaseGoodId: foundReleaseGood.id
      }).getMany()

      if (orderInventoryByBin.length > 0) {
        // Pass the release good to indicate this is a release good scan
        return transformToWorksheetFormat(orderInventoryByBin, foundReleaseGood)
      } else {
        throw new OutboundOrderError('There is no item in this order', 'NO_ITEMS_IN_ORDER')
      }
    } else {
      // Try to find by bin location
      const binLocation: Location = await tx.getRepository(Location).findOne({
        where: { domain, name: orderNo }
      })

      if (binLocation) {
        orderInventoryByBin = await buildOrderInventoryQuery(tx, domain.id, { locationId: binLocation.id }).getMany()
      }

      if (orderInventoryByBin.length > 0) {
        return transformToWorksheetFormat(orderInventoryByBin)
      } else {
        throw new OutboundOrderError('There is no item in this bin', 'NO_ITEMS_IN_BIN')
      }
    }
  }
}
