// Shared item model for the DIGITAL picklist (<FulfillmentDigitalPicklist>): turning
// m_inout records into pickable lines, aggregating them and sorting them.
//
// Every host (/generate-shipments panel, /integrations/picklist-desktop, /mobile/picklist,
// /mobile/generate-shipments) feeds the component with the lines returned by
// `pickLinesFromInOut(record)` — the item lines of one shipment, each tagged with the
// shipment it came from (`_docNo`, `_shipper`, `_partner`). The component aggregates
// them per org + product + locator (same key as the printed picklist) and keeps the
// shipment / shipper sets per aggregated row so the picker can sort by them WITHOUT
// splitting a row that is shared by several shipments.
//
// The PDF paths of the hosts keep their own (older) aggregation + `sortPicklistData`;
// this file is only for the digital list.

import { resolvePickCode, getProductCodeValue, PICK_CODE_KEYS } from './pickCode'

export interface PicklistShipmentRef {
  docNo: string
  id: any
  partner: string
  org: string
  shipper: string
  lineCount: number
  totalQty: number
}

export const PICKLIST_SORT_KEYS = ['locator', 'pickcode', 'value', 'sku', 'mpn', 'qty', 'shipper', 'docno', 'org'] as const
export type PicklistSortKey = typeof PICKLIST_SORT_KEYS[number]

// A shipment line counts as a pickable item only when its product is ProductType 'I'
// (iDempiere "Item" — physical stock, not services/charges). iDempiere returns list
// fields as { id: 'I', identifier: 'Item' } when the product is expanded; plain GETs
// may return a bare string instead.
export const isItemProduct = (product: any): boolean => {
  const raw = product?.ProductType ?? product?.producttype
  const code = (raw && typeof raw === 'object') ? (raw.id ?? raw.identifier ?? '') : (raw ?? '')
  return code.toString().trim().toUpperCase() === 'I'
}

const fkName = (fk: any): string => {
  if (!fk) return ''
  if (typeof fk === 'object') return (fk.identifier ?? fk.Name ?? fk.name ?? '').toString()
  return fk.toString()
}

// Item lines of one m_inout record (from /api/processes/inouts/{docNo}), each tagged with
// its shipment. Keeps only physical items with a locator and no charge.
export const pickLinesFromInOut = (record: any): any[] => {
  if (!record) return []
  const docNo = (record.DocumentNo ?? '').toString()
  const shipper = fkName(record.M_Shipper_ID)
  const partner = fkName(record.C_BPartner_ID)
  const org = fkName(record.AD_Org_ID)
  return (record.m_inoutline || [])
    .filter((l: any) => l?.M_Product_ID && l?.M_Locator_ID && !l?.C_Charge_ID && isItemProduct(l.M_Product_ID))
    .map((l: any) => ({ ...l, _docNo: docNo, _inoutId: record.id, _shipper: shipper, _partner: partner, _org: org }))
}

// Stable identity of an aggregated row: org + product + locator slot (same as the PDF).
export const picklistItemId = (line: any): string =>
  `${line?.AD_Org_ID?.id}-${line?.M_Product_ID?.Value}-${line?.M_Locator_ID?.X}-${line?.M_Locator_ID?.Y}-${line?.M_Locator_ID?.Z}`

// One shipment summary per distinct `_docNo` in the given lines.
export const picklistShipmentsFromLines = (lines: any[]): PicklistShipmentRef[] => {
  const map = new Map<string, PicklistShipmentRef>()
  for (const line of lines || []) {
    const docNo = (line?._docNo ?? '').toString()
    if (!docNo) continue
    let ref = map.get(docNo)
    if (!ref) {
      ref = { docNo, id: line._inoutId, partner: line._partner || '', org: line._org || fkName(line.AD_Org_ID), shipper: line._shipper || '', lineCount: 0, totalQty: 0 }
      map.set(docNo, ref)
    }
    ref.lineCount += 1
    ref.totalQty += Number(line?.QtyEntered || 0)
  }
  return [...map.values()]
}

// Group the same product/locator across all shipments. The result rows carry the
// summed quantity plus `_docNos` / `_shippers` (sorted, unique) for display and sorting.
export const aggregatePicklistLines = (lines: any[]): any[] => {
  const byId = new Map<string, any>()
  for (const line of lines || []) {
    const id = picklistItemId(line)
    const existing = byId.get(id)
    if (existing) {
      existing.QtyEntered = Number(existing.QtyEntered) + Number(line.QtyEntered || 0)
      if (line._docNo && !existing._docNos.includes(line._docNo)) existing._docNos.push(line._docNo)
      if (line._shipper && !existing._shippers.includes(line._shipper)) existing._shippers.push(line._shipper)
    } else {
      byId.set(id, {
        ...line,
        _id: id,
        QtyEntered: Number(line.QtyEntered || 0),
        _docNos: line._docNo ? [line._docNo] : [],
        _shippers: line._shipper ? [line._shipper] : [],
      })
    }
  }
  const rows = [...byId.values()]
  for (const row of rows) {
    row._docNos.sort((a: string, b: string) => a.localeCompare(b, undefined, { numeric: true }))
    row._shippers.sort()
  }
  return rows
}

const natural = (a: any, b: any) =>
  (a ?? '').toString().localeCompare((b ?? '').toString(), undefined, { numeric: true, sensitivity: 'base' })

// Ranking used by the "pick X first" override (locator sort only): rotates the aisle
// order so `startX` leads, the aisles "after" it follow, then the ones before wrap round.
export const locatorRotationRank = (x: any, startX: any, dir: number) => {
  const c = natural(x, startX)
  if (dir === -1) return c <= 0 ? 0 : 1
  return c >= 0 ? 0 : 1
}

// The value the primary pick code resolves to (partner identifier → product PickListCode → fallback).
export const primaryPickCodeValue = (item: any): string => {
  const resolved = resolvePickCode(item?.M_Product_ID, item?.pickIdentifier)
  return resolved ? getProductCodeValue(item.M_Product_ID, resolved.key) : ''
}

export interface PicklistSortOptions {
  sortBy: PicklistSortKey | ''
  dir?: 'asc' | 'desc'
  startX?: string
}

// Organization is always the primary grouping (ascending); `dir` only flips the chosen field.
export const sortPicklistItems = (items: any[], { sortBy, dir = 'asc', startX = '' }: PicklistSortOptions): any[] => {
  const d = dir === 'desc' ? -1 : 1
  return [...(items || [])].sort((a, b) => {
    if (sortBy !== 'org') {
      const orgCompare = natural(a.AD_Org_ID?.identifier, b.AD_Org_ID?.identifier)
      if (orgCompare !== 0) return orgCompare
    }
    let cmp = 0
    switch (sortBy) {
      case 'org':
        cmp = natural(a.AD_Org_ID?.identifier, b.AD_Org_ID?.identifier)
        break
      case 'value':
        cmp = natural(a.M_Product_ID?.Value, b.M_Product_ID?.Value)
        break
      case 'sku':
        cmp = natural(a.M_Product_ID?.SKU, b.M_Product_ID?.SKU)
        break
      case 'mpn':
        cmp = natural(a.M_Product_ID?.mpn || a.M_Product_ID?.MPN, b.M_Product_ID?.mpn || b.M_Product_ID?.MPN)
        break
      case 'pickcode':
        cmp = natural(primaryPickCodeValue(a), primaryPickCodeValue(b))
        break
      case 'qty':
        // asc = lowest first, desc = highest first
        cmp = Number(a.QtyEntered) - Number(b.QtyEntered)
        break
      case 'shipper':
        cmp = natural((a._shippers || []).join(', '), (b._shippers || []).join(', '))
        break
      case 'docno':
        cmp = natural((a._docNos || [])[0], (b._docNos || [])[0])
        break
      case 'locator': {
        if (startX) {
          const aRank = locatorRotationRank(a.M_Locator_ID?.X, startX, d)
          const bRank = locatorRotationRank(b.M_Locator_ID?.X, startX, d)
          if (aRank !== bRank) return aRank - bRank
        }
        cmp = natural(a.M_Locator_ID?.X, b.M_Locator_ID?.X)
          || natural(a.M_Locator_ID?.Y, b.M_Locator_ID?.Y)
          || natural(a.M_Locator_ID?.Z, b.M_Locator_ID?.Z)
        break
      }
      default:
        return 0
    }
    // Stable tie-break so equal keys keep the walking order through the warehouse.
    if (cmp === 0 && sortBy !== 'locator') {
      cmp = natural(a.M_Locator_ID?.X, b.M_Locator_ID?.X)
        || natural(a.M_Locator_ID?.Y, b.M_Locator_ID?.Y)
        || natural(a.M_Locator_ID?.Z, b.M_Locator_ID?.Z)
      return cmp
    }
    return d * cmp
  })
}

// All codes of a product a scanner / search may hit (upper-cased, trimmed).
export const productScanCodes = (product: any): string[] => {
  const codes: string[] = []
  for (const key of PICK_CODE_KEYS) {
    const v = getProductCodeValue(product, key)
    if (v) codes.push(v.toString().trim().toUpperCase())
  }
  return codes
}

// Does a scanned/typed code identify this item? Exact match on any product code; an
// EAN-13 scan also matches a stored UPC-A (12 digits) with the leading zero dropped.
export const itemMatchesScan = (item: any, code: string): boolean => {
  const needle = (code ?? '').toString().trim().toUpperCase()
  if (!needle) return false
  const codes = productScanCodes(item?.M_Product_ID)
  if (codes.includes(needle)) return true
  if (/^\d{13}$/.test(needle) && needle.startsWith('0') && codes.includes(needle.slice(1))) return true
  if (/^\d{12}$/.test(needle) && codes.includes('0' + needle)) return true
  return false
}
