import { string } from 'alga-js'
import refreshTokenHelper from "../../../utils/refreshTokenHelper"
import forceLogoutHelper from "../../../utils/forceLogoutHelper"
import errorHandlingHelper from "../../../utils/errorHandlingHelper"
import fetchHelper from "../../../utils/fetchHelper"
import getTokenHelper from "../../../utils/getTokenHelper"

// iDempiere shipment generation (MStorageOnHand.getWarehouse) allocates stock
// ORDER BY l.PriorityNo DESC — the locator with the HIGHER PriorityNo is picked
// first; equal priorities fall back to material-policy date, which splits a
// product's picks across locators ("round robin"). This endpoint finds products
// with qty on hand on 2+ locators inside the SAME warehouse and proposes
// PriorityNo values so the locator with the LOWEST Z (lowest level, easiest to
// reach) is picked first. Ties broken by X, then Y, then locator Value.

// X/Y/Z are VARCHAR in iDempiere — compare numeric when both parse, natural-sort
// otherwise. Empty/unknown sorts LAST (→ lowest priority → picked last).
const compareCoord = (a: any, b: any): number => {
  const sa = String(a ?? '').trim()
  const sb = String(b ?? '').trim()
  if (sa === '' && sb === '') return 0
  if (sa === '') return 1
  if (sb === '') return -1
  const na = Number(sa)
  const nb = Number(sb)
  if (Number.isFinite(na) && Number.isFinite(nb)) return na - nb
  return sa.localeCompare(sb, undefined, { numeric: true })
}

// Desired pick order: lowest Z first, then X, Y, Value, id as deterministic tie-breaks.
const compareLocatorsAsc = (l1: any, l2: any): number => {
  return compareCoord(l1.z, l2.z)
    || compareCoord(l1.x, l2.x)
    || compareCoord(l1.y, l2.y)
    || String(l1.value ?? '').localeCompare(String(l2.value ?? ''), undefined, { numeric: true })
    || (Number(l1.id) - Number(l2.id))
}

const handleFunc = async (event: any, authToken: any = null) => {
  const token = authToken ?? await getTokenHelper(event)
  const query = getQuery(event)
  const orgId = query?.org_id as string

  if (!orgId) {
    return { status: 400, message: 'Organization ID is required', records: [], 'row-count': 0, summary: null }
  }

  // Step 1: All locators of the org (full records so M_Warehouse_ID is present)
  const locatorMap = new Map<number, any>()
  let locatorsCapped = false
  const locatorOrgFilter = string.urlEncode(`AD_Org_ID eq ${orgId}`)
  const locatorRes: any = await fetchHelper(
    event,
    `models/m_locator?$filter=${locatorOrgFilter}&$expand=M_LocatorType_ID&$top=5000`,
    'GET',
    token,
    null
  )
  if (locatorRes?.records) {
    locatorsCapped = locatorRes.records.length >= 5000
    for (const loc of locatorRes.records) {
      locatorMap.set(loc.id, {
        id: loc.id,
        value: loc.Value || '',
        x: loc.X ?? '',
        y: loc.Y ?? '',
        z: loc.Z ?? '',
        priorityNo: Number(loc.PriorityNo ?? 0),
        warehouseId: loc.M_Warehouse_ID?.id ?? null,
        warehouseName: loc.M_Warehouse_ID?.identifier ?? '',
        isActive: loc.IsActive === true || loc.IsActive === 'Y',
        locatorTypeName: loc.M_LocatorType_ID?.Name || ''
      })
    }
  }

  // Step 2: Products of the org with their storage records
  const orgFilter = string.urlEncode(`AD_Org_ID eq ${orgId} and IsActive eq true`)
  const res: any = await fetchHelper(
    event,
    `models/m_product?$select=${string.urlEncode('M_Product_ID,Name,SKU,UPC,Value,mpn,AD_Org_ID')}&$expand=M_Storage,AD_Org_ID($select=Name)&$filter=${orgFilter}&$orderby=${string.urlEncode('Name asc')}&$top=5000`,
    'GET',
    token,
    null
  )

  if (!res?.records) {
    return { status: 200, records: [], 'row-count': 0, summary: null, capped: locatorsCapped }
  }
  const productsCapped = res.records.length >= 5000

  // Step 3: Per product, sum qty per locator (ASI rows sum first), keep qty > 0,
  // then group by warehouse — a (product, warehouse) group with 2+ locators is a
  // conflict group. Stock split across DIFFERENT warehouses is NOT a conflict
  // (shipment generation is warehouse-scoped).
  let skippedStorageRows = 0
  // conflictGroups: { product, warehouseId, locators: [{ locator, qty }] }
  const conflictGroups: any[] = []

  for (const product of res.records) {
    const storages = product?.M_Storage || []
    if (!Array.isArray(storages) || storages.length === 0) continue

    const qtyByLocator = new Map<number, number>()
    for (const storage of storages) {
      const locId = storage?.M_Locator_ID?.id
      if (!locId || !locatorMap.has(locId)) {
        skippedStorageRows++
        continue
      }
      qtyByLocator.set(locId, (qtyByLocator.get(locId) ?? 0) + Number(storage?.QtyOnHand || 0))
    }

    const byWarehouse = new Map<number, any[]>()
    for (const [locId, qty] of qtyByLocator) {
      if (qty <= 0) continue
      const locator = locatorMap.get(locId)
      if (locator.warehouseId == null) continue
      if (!byWarehouse.has(locator.warehouseId)) byWarehouse.set(locator.warehouseId, [])
      byWarehouse.get(locator.warehouseId)!.push({ locator, qty })
    }

    for (const [warehouseId, entries] of byWarehouse) {
      if (entries.length >= 2) {
        conflictGroups.push({ product, warehouseId, entries })
      }
    }
  }

  // Step 4: Violation check per conflict group — locators in desired pick order
  // (compareLocatorsAsc) must have STRICTLY decreasing PriorityNo. Equal counts
  // as violated (ambiguous order is exactly the round-robin symptom).
  const involvedByWarehouse = new Map<number, Set<number>>()
  const violatedWarehouses = new Set<number>()
  const violatedProductWh = new Set<string>()

  for (const group of conflictGroups) {
    group.entries.sort((a: any, b: any) => compareLocatorsAsc(a.locator, b.locator))

    if (!involvedByWarehouse.has(group.warehouseId)) involvedByWarehouse.set(group.warehouseId, new Set())
    const involved = involvedByWarehouse.get(group.warehouseId)!
    for (const entry of group.entries) involved.add(entry.locator.id)

    let violated = false
    for (let i = 1; i < group.entries.length; i++) {
      if (group.entries[i].locator.priorityNo >= group.entries[i - 1].locator.priorityNo) {
        violated = true
        break
      }
    }
    group.violated = violated
    if (violated) {
      violatedWarehouses.add(group.warehouseId)
      violatedProductWh.add(`${group.product.id}_${group.warehouseId}`)
    }
  }

  // Step 5: Rank-based proposals per violated warehouse — sort the warehouse's
  // involved locators into desired pick order and assign strictly decreasing
  // priorities (first/lowest Z gets the highest number). Deterministic and
  // idempotent: re-running after apply yields proposed == current everywhere.
  const proposals = new Map<number, number>()
  for (const warehouseId of violatedWarehouses) {
    const involved = Array.from(involvedByWarehouse.get(warehouseId)!)
      .map(id => locatorMap.get(id))
      .sort(compareLocatorsAsc)
    const n = involved.length
    const step = n * 10 <= 9999 ? 10 : Math.max(1, Math.floor(9999 / n))
    for (let i = 0; i < n; i++) {
      proposals.set(involved[i].id, (n - i) * step)
    }
  }

  // Step 6: Build response rows — one per (conflict-group product, locator)
  const records: any[] = []
  for (const group of conflictGroups) {
    const product = group.product
    const productId = product.M_Product_ID || product.id
    for (const entry of group.entries) {
      const locator = entry.locator
      const proposal = proposals.get(locator.id)
      const proposedPriority = proposal !== undefined && proposal !== locator.priorityNo ? proposal : null
      records.push({
        id: `${productId}_${locator.id}`,
        kind: 'priority',
        productId,
        productName: product.Name || '',
        productValue: product.Value || '',
        sku: product.SKU || '',
        ean: product.UPC || '',
        mpn: product.mpn || '',
        warehouseId: group.warehouseId,
        warehouseName: locator.warehouseName,
        locatorId: locator.id,
        locatorValue: locator.value,
        locatorX: locator.x,
        locatorY: locator.y,
        locatorZ: locator.z,
        locatorTypeName: locator.locatorTypeName,
        locatorIsActive: locator.isActive,
        qtyOnHand: entry.qty,
        currentPriority: locator.priorityNo,
        proposedPriority,
        status: proposedPriority !== null ? 'change' : 'ok',
        productViolated: group.violated === true
      })
    }
  }

  records.sort((a, b) =>
    String(a.warehouseName).localeCompare(String(b.warehouseName))
    || String(a.productName).localeCompare(String(b.productName))
    || compareLocatorsAsc(
      { z: a.locatorZ, x: a.locatorX, y: a.locatorY, value: a.locatorValue, id: a.locatorId },
      { z: b.locatorZ, x: b.locatorX, y: b.locatorY, value: b.locatorValue, id: b.locatorId }
    )
  )

  const changedLocatorIds = new Set(records.filter(r => r.status === 'change').map(r => r.locatorId))

  return {
    status: 200,
    records,
    'row-count': records.length,
    summary: {
      productsMultiLocator: new Set(conflictGroups.map(g => `${g.product.id}_${g.warehouseId}`)).size,
      productsViolated: violatedProductWh.size,
      warehousesAffected: violatedWarehouses.size,
      locatorsToChange: changedLocatorIds.size,
      skippedStorageRows
    },
    capped: locatorsCapped || productsCapped
  }
}

export default defineEventHandler(async (event) => {
  let data: any = {}

  try {
    data = await handleFunc(event)
  } catch (err: any) {
    try {
      let authToken: any = await refreshTokenHelper(event)
      data = await handleFunc(event, authToken)
    } catch (error: any) {
      data = errorHandlingHelper(err?.data ?? err, error?.data ?? error)
      forceLogoutHelper(event, data)
    }
  }

  return data
})
