import { string } from 'alga-js'

/**
 * Per-org cache of locator id sets resolved by locator-type flags. The
 * locator-type/locator config is admin-static and changes very rarely, but these
 * helpers are called once PER PRODUCT on hot paths (e.g. /api/local/qty-on-hand,
 * which the FFN stock page now fires up to ~1000× per load). Without caching,
 * each call ran 2 extra iDempiere queries. Cached per Nitro process with a short
 * TTL (fail-soft; never caches an error).
 */
const LOCATOR_IDS_TTL_MS = 10 * 60 * 1000
const locatorIdsCache: Map<string, { ids: Set<number>; expires: number }> = new Map()

/**
 * Resolves the ids of all active locators whose M_LocatorType matches the given
 * OData filter. Locators without a locator type never match.
 */
const getLocatorIdsByTypeFilter = async (
  event: any,
  token: string,
  orgId: number | string | null,
  typeFilterRaw: string,
  cachePrefix: string
): Promise<Set<number>> => {
  const cacheKey = `${cachePrefix}:${orgId ?? 'all'}`
  const cached = locatorIdsCache.get(cacheKey)
  if (cached && cached.expires > Date.now()) return cached.ids

  const result = new Set<number>()

  try {
    const typeFilter = string.urlEncode(typeFilterRaw)
    const ltRes: any = await event.context.fetch(
      `models/m_locatortype?$filter=${typeFilter}&$top=1000`,
      'GET',
      token,
      null
    )
    const typeIds = (ltRes?.records || []).map((t: any) => t?.id).filter((id: any) => id !== undefined && id !== null)
    if (typeIds.length === 0) {
      // Valid "no matching locators" answer — safe to cache.
      locatorIdsCache.set(cacheKey, { ids: result, expires: Date.now() + LOCATOR_IDS_TTL_MS })
      return result
    }

    const typeFilterParts = typeIds.map((id: number) => `M_LocatorType_ID eq ${id}`).join(' OR ')
    let locFilter = `(${typeFilterParts}) AND IsActive eq true`
    if (orgId !== null && orgId !== undefined && orgId !== '') {
      locFilter += ` AND AD_Org_ID eq ${orgId}`
    }

    const locRes: any = await event.context.fetch(
      `models/m_locator?$filter=${string.urlEncode(locFilter)}&$top=10000`,
      'GET',
      token,
      null
    )
    for (const loc of (locRes?.records || [])) {
      if (loc?.id !== undefined && loc?.id !== null) result.add(loc.id)
    }
  } catch (err) {
    console.error(`[getLocatorIdsByTypeFilter:${cachePrefix}] Error:`, err)
    // Do NOT cache on error — return a (possibly partial) result for this call only.
    return result
  }

  locatorIdsCache.set(cacheKey, { ids: result, expires: Date.now() + LOCATOR_IDS_TTL_MS })
  return result
}

/**
 * Locators whose M_LocatorType has all three IsAvailableFor* flags set to
 * false are considered "return" locators. Stock sitting on them should be
 * reported separately from the regular on-hand quantity.
 */
const getReturnLocatorIds = async (
  event: any,
  token: string,
  orgId: number | string | null = null
): Promise<Set<number>> => {
  return getLocatorIdsByTypeFilter(
    event,
    token,
    orgId,
    'IsAvailableForReplenishment eq false AND IsAvailableForReservation eq false AND IsAvailableForShipping eq false AND IsActive eq true',
    'return'
  )
}

/**
 * Locators whose M_LocatorType allows neither shipping nor reservation
 * (regardless of the replenishment flag). Receipts must never auto-preset
 * these as a destination — a superset of the return locators above.
 */
export const getNonReceivableLocatorIds = async (
  event: any,
  token: string,
  orgId: number | string | null = null
): Promise<Set<number>> => {
  return getLocatorIdsByTypeFilter(
    event,
    token,
    orgId,
    'IsAvailableForShipping eq false AND IsAvailableForReservation eq false AND IsActive eq true',
    'nonreceivable'
  )
}

export default getReturnLocatorIds
