/**
 * SIS (Sales Information System) — matches Amazon's bulk merchant-listings
 * report rows (see amazonSpApi.ts -> getMerchantListingsReport) to local
 * m_product records and joins in the internal qty-on-hand and sales price.
 *
 * Match order matters: `seller-sku` should always equal a LogShip
 * M_Product.Value or M_Product.SKU (that's what gets shipped as the SKU when
 * listing), so SKU is the primary key. m_product.asin is NOT always
 * populated in LogShip, so it is a fallback used only for rows the SKU tiers
 * miss — do not flip this order.
 */
import { string } from 'alga-js'
import fetchHelper from './fetchHelper'
import { fetchStorageRowsForProducts, groupStorageByProduct } from './productStorageHelper'
import getReturnLocatorIds from './returnLocatorHelper'
import type { MerchantListingRow } from './amazonSpApi'

export interface SisRow {
  sellerSku: string
  asin: string
  amazonItemName: string
  amazonPrice: number | null
  amazonQty: number | null
  fulfillmentChannel: string
  status: string
  openDate: string | null
  productId: number | null
  productName: string
  productValue: string
  strapiProductDocumentId: string | null
  isBom: boolean
  internalPrice: number | null
  internalQtyOnHand: number | null
  returnQtyOnHand: number
}

const CHUNK_SIZE = 80

// Same combined-flag pattern used across the app (OrderPositionsTable.vue,
// ProductSearchSelect.vue, SearchModal.vue, …) — a product counts as BOM
// when either the native IsBOM flag or the JTL-synced isJtlBom flag is set.
const isBomFlag = (product: any): boolean => {
  const v = product?.IsBOM ?? product?.isBOM
  const j = product?.isJtlBom ?? product?.IsJtlBom
  return v === true || v === 'Y' || j === true || j === 'Y'
}

const chunk = <T,>(arr: T[], size: number): T[][] => {
  const out: T[][] = []
  for (let i = 0; i < arr.length; i += size) out.push(arr.slice(i, i + size))
  return out
}

// Runs `fn` over every chunk with bounded concurrency instead of one at a
// time — these are plain OData filter queries against m_product (no
// $expand), so they don't hit the synchronized-lock path the "iDempiere REST
// load rules" recipe warns about; a cap (rather than unbounded Promise.all)
// just avoids opening dozens of simultaneous connections on a large catalog.
const CHUNK_CONCURRENCY = 5
async function forEachChunk<T>(chunks: T[][], fn: (c: T) => Promise<void>): Promise<void> {
  let next = 0
  const workers = Array.from({ length: Math.min(CHUNK_CONCURRENCY, chunks.length) }, async () => {
    for (;;) {
      const i = next++
      if (i >= chunks.length) return
      await fn(chunks[i])
    }
  })
  await Promise.all(workers)
}

// OData string literals are single-quoted; escape embedded quotes so a SKU
// like O'Brien-123 can't break the filter.
const escapeOData = (v: string): string => v.replace(/'/g, "''")

/**
 * Resolves the current active m_pricelist_version for the app's default
 * price list (runtimeConfig.public.pricelistid, the same "101" default used
 * app-wide for new orders — see OrderQuickAddModal.vue). Cached briefly per
 * process since this rarely changes.
 */
const priceListVersionCache = new Map<string, { id: number | null; at: number }>()
const PRICE_LIST_VERSION_TTL_MS = 10 * 60 * 1000

export async function resolveActivePriceListVersionId(event: any, token: string, priceListId: string | number): Promise<number | null> {
  const key = String(priceListId)
  const cached = priceListVersionCache.get(key)
  if (cached && Date.now() - cached.at < PRICE_LIST_VERSION_TTL_MS) return cached.id

  let id: number | null = null
  try {
    const filter = `M_PriceList_ID eq ${Number(priceListId)} and isActive eq true`
    const res: any = await fetchHelper(
      event,
      `models/m_pricelist_version?$filter=${string.urlEncode(filter)}&$orderby=${string.urlEncode('ValidFrom desc')}&$top=1`,
      'GET',
      token,
      null
    )
    id = res?.records?.[0]?.id ?? null
  } catch {
    id = null
  }
  priceListVersionCache.set(key, { id, at: Date.now() })
  return id
}

/** Batch-resolves M_Product records matching a set of SKUs (Value or SKU field) or ASINs, scoped to one org. */
async function fetchMatchCandidates(
  event: any,
  token: string,
  orgId: number,
  skus: string[],
  asins: string[]
): Promise<{ bySku: Map<string, any>; byAsin: Map<string, any> }> {
  const bySku = new Map<string, any>()
  const byAsin = new Map<string, any>()

  await Promise.all([
    forEachChunk(chunk(skus, CHUNK_SIZE), async (skuChunk) => {
      if (skuChunk.length === 0) return
      const orClause = skuChunk
        .map(s => `Value eq '${escapeOData(s)}' or SKU eq '${escapeOData(s)}'`)
        .join(' or ')
      const filter = `AD_Org_ID eq ${orgId} and (${orClause})`
      const res: any = await fetchHelper(event, `models/m_product?$filter=${string.urlEncode(filter)}&$top=500`, 'GET', token, null)
      for (const p of (res?.records || [])) {
        if (p.Value) bySku.set(String(p.Value).toLowerCase(), p)
        if (p.SKU) bySku.set(String(p.SKU).toLowerCase(), p)
      }
    }),
    forEachChunk(chunk(asins, CHUNK_SIZE), async (asinChunk) => {
      if (asinChunk.length === 0) return
      const orClause = asinChunk.map(a => `asin eq '${escapeOData(a)}'`).join(' or ')
      const filter = `AD_Org_ID eq ${orgId} and (${orClause})`
      const res: any = await fetchHelper(event, `models/m_product?$filter=${string.urlEncode(filter)}&$top=500`, 'GET', token, null)
      for (const p of (res?.records || [])) {
        if (p.asin) byAsin.set(String(p.asin).toLowerCase(), p)
      }
    })
  ])

  return { bySku, byAsin }
}

/** Batch-resolves m_productprice.priceStd for a set of product ids at one price-list version. */
async function fetchPricesForProducts(
  event: any,
  token: string,
  productIds: number[],
  priceListVersionId: number | null
): Promise<Map<number, number>> {
  const prices = new Map<number, number>()
  if (!priceListVersionId || productIds.length === 0) return prices

  await forEachChunk(chunk(productIds, CHUNK_SIZE), async (idChunk) => {
    const filter = `M_PriceList_Version_ID eq ${priceListVersionId} and M_Product_ID in (${idChunk.join(',')})`
    const res: any = await fetchHelper(event, `models/m_productprice?$filter=${string.urlEncode(filter)}`, 'GET', token, null)
    for (const pp of (res?.records || [])) {
      const pid = Number(pp?.M_Product_ID?.id ?? pp?.M_Product_ID)
      const price = Number(pp?.priceStd ?? pp?.PriceStd)
      if (Number.isFinite(pid) && Number.isFinite(price)) prices.set(pid, price)
    }
  })
  return prices
}

/**
 * Joins Amazon listing rows to local products. Rows with no local match are
 * kept (productId: null) rather than dropped — a listing live on Amazon with
 * nothing tracked internally is useful information on its own.
 */
export async function matchListingsToProducts(
  event: any,
  token: string,
  orgId: number,
  listings: MerchantListingRow[],
  priceListId: string | number
): Promise<SisRow[]> {
  const skus = [...new Set(listings.map(l => l.sellerSku).filter(Boolean))]
  const asins = [...new Set(listings.map(l => l.asin).filter(Boolean))]

  const [{ bySku, byAsin }, priceListVersionId, returnLocatorIds] = await Promise.all([
    fetchMatchCandidates(event, token, orgId, skus, asins),
    resolveActivePriceListVersionId(event, token, priceListId),
    getReturnLocatorIds(event, token, orgId)
  ])

  const resolved = listings.map(l => {
    const bySkuMatch = bySku.get(l.sellerSku.toLowerCase())
    const product = bySkuMatch || (l.asin ? byAsin.get(l.asin.toLowerCase()) : null) || null
    return { listing: l, product }
  })

  const matchedIds = [...new Set(resolved.filter(r => r.product).map(r => Number(r.product.id)))]
  const [storageRows, prices] = await Promise.all([
    matchedIds.length ? fetchStorageRowsForProducts(event, token, matchedIds) : Promise.resolve([]),
    fetchPricesForProducts(event, token, matchedIds, priceListVersionId)
  ])
  const storageByProduct = groupStorageByProduct(storageRows)

  const qtyOnHandFor = (productId: number) => {
    const rows = storageByProduct.get(productId) || []
    let onHand = 0
    let returnOnHand = 0
    for (const s of rows) {
      const qty = Number(s?.QtyOnHand ?? 0)
      const locId = Number(s?.M_Locator_ID?.id ?? s?.M_Locator_ID)
      if (returnLocatorIds.has(locId)) returnOnHand += qty
      else onHand += qty
    }
    return { onHand, returnOnHand }
  }

  return resolved.map(({ listing, product }) => {
    const productId = product ? Number(product.id) : null
    const { onHand, returnOnHand } = productId ? qtyOnHandFor(productId) : { onHand: 0, returnOnHand: 0 }
    return {
      sellerSku: listing.sellerSku,
      asin: listing.asin,
      amazonItemName: listing.itemName,
      amazonPrice: listing.price,
      amazonQty: listing.quantity,
      fulfillmentChannel: listing.fulfillmentChannel,
      status: listing.status,
      openDate: listing.openDate,
      productId,
      productName: product?.Name || '',
      productValue: product?.Value || product?.SKU || '',
      strapiProductDocumentId: product?.Strapi_Product_documentId || null,
      isBom: product ? isBomFlag(product) : false,
      internalPrice: productId ? (prices.get(productId) ?? null) : null,
      internalQtyOnHand: productId ? onHand : null,
      returnQtyOnHand: returnOnHand
    }
  })
}
