import { string } from 'alga-js'
import fetchHelper from './fetchHelper'

/**
 * Product lists WITHOUT `$expand=M_Storage`.
 *
 * Why: in idempiere-rest a detail expand (`$expand=M_Storage` on m_product) runs
 * `ExpandParser.getChildPOs → DefaultQueryConverter.convertStatement` once per
 * parent record, and that method is `synchronized` on a singleton. A list of
 * 3.3k products therefore becomes ~6.6k serialized queries under ONE global
 * lock; under CPU pressure a single call exceeds nginx's 60 s and, once
 * retried, queues every other API request behind it (2026-09-10 outage:
 * 178/200 Jetty threads blocked on that monitor).
 *
 * Instead: fetch the products plainly, fetch the m_storage rows in a handful
 * of paged list calls (7.9k rows today → 4 calls), and join in Node. Records
 * get the same `M_Storage: [...]` array shape the expand produced, so the
 * consumers (qty-on-hand pages, ag-grid, priority recommendations, the
 * all-products list) don't change.
 */

const STORAGE_PAGE = 2000
const IDS_PER_CHUNK = 100
// Storage rows are only cached for the duration of a page load burst: several
// endpoints of the same screen (overview + by-locator) ask for the same set.
const STORAGE_CACHE_TTL_MS = 30_000

type StorageMap = Map<number, any[]>

const storageCache = new Map<string, { at: number; rows: any[] }>()
const storageInflight = new Map<string, Promise<any[]>>()

function cacheKey(event: any): string {
  // Visibility of m_storage rows depends on the iDempiere client/role/org of
  // the session, never on the user — cache per that triple.
  const c = getCookie(event, 'logship_client_id') || ''
  const r = getCookie(event, 'logship_role_id') || ''
  const o = getCookie(event, 'logship_organization_id') || ''
  return `${c}:${r}:${o}`
}

async function fetchStoragePages(event: any, token: any, filter: string | null): Promise<any[]> {
  const rows: any[] = []
  let skip = 0
  for (;;) {
    const f = filter ? `&$filter=${string.urlEncode(filter)}` : ''
    const res: any = await fetchHelper(
      event,
      `models/m_storage?$orderby=${string.urlEncode('M_Storage_ID asc')}${f}&$top=${STORAGE_PAGE}&$skip=${skip}`,
      'GET',
      token,
      null
    )
    const page: any[] = Array.isArray(res?.records) ? res.records : []
    rows.push(...page)
    if (page.length < STORAGE_PAGE) break
    skip += STORAGE_PAGE
    if (skip > 200_000) break // paranoia cap
  }
  return rows
}

/** All m_storage rows visible to this session, cached + single-flight. */
export async function fetchAllStorageRows(event: any, token: any): Promise<any[]> {
  const key = cacheKey(event)
  const hit = storageCache.get(key)
  if (hit && Date.now() - hit.at < STORAGE_CACHE_TTL_MS) return hit.rows
  const running = storageInflight.get(key)
  if (running) return running
  const p = fetchStoragePages(event, token, null)
    .then((rows) => {
      storageCache.set(key, { at: Date.now(), rows })
      return rows
    })
    .finally(() => storageInflight.delete(key))
  storageInflight.set(key, p)
  return p
}

/** m_storage rows for a specific (small) set of products — one call per 100 ids. */
export async function fetchStorageRowsForProducts(event: any, token: any, productIds: number[]): Promise<any[]> {
  const ids = [...new Set(productIds.filter((n) => Number.isFinite(n) && n > 0))]
  const rows: any[] = []
  for (let i = 0; i < ids.length; i += IDS_PER_CHUNK) {
    const chunk = ids.slice(i, i + IDS_PER_CHUNK)
    rows.push(...await fetchStoragePages(event, token, `M_Product_ID in (${chunk.join(',')})`))
  }
  return rows
}

export function groupStorageByProduct(rows: any[]): StorageMap {
  const map: StorageMap = new Map()
  for (const row of rows) {
    const pid = Number(row?.M_Product_ID?.id ?? row?.M_Product_ID)
    if (!Number.isFinite(pid)) continue
    const list = map.get(pid)
    if (list) list.push(row)
    else map.set(pid, [row])
  }
  return map
}

/** Sets `M_Storage` on every product record (same shape as the old expand). */
export function attachStorage(records: any[], storage: StorageMap): any[] {
  return records.map((p: any) => ({ ...p, M_Storage: storage.get(Number(p?.id)) ?? [] }))
}

/**
 * Drop-in replacement for `models/m_product?...&$expand=M_Storage[,other]...`.
 * Pass the product query WITHOUT the M_Storage expand; the helper picks the
 * cheaper storage strategy (by ids for a page, all rows for a full list).
 */
export async function fetchProductsWithStorage(event: any, token: any, productUrl: string): Promise<any> {
  const res: any = await fetchHelper(event, productUrl, 'GET', token, null)
  const records: any[] = Array.isArray(res?.records) ? res.records : []
  if (records.length === 0) return res

  const ids = records.map((p: any) => Number(p?.id)).filter((n) => Number.isFinite(n) && n > 0)
  const rows = ids.length <= IDS_PER_CHUNK * 3
    ? await fetchStorageRowsForProducts(event, token, ids)
    : await fetchAllStorageRows(event, token)

  return { ...res, records: attachStorage(records, groupStorageByProduct(rows)) }
}
