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

const handleFunc = async (event: any, authToken: any = null) => {
  let data: any = {}
  const token = authToken ?? await getTokenHelper(event)
  const body = await readBody(event)
  const searchTerm = body?.search || ''
  const includeAllOrgs = body?.includeAllOrgs || false
  const filterOrgId = body?.organizationId || null
  // Opt-in BOM exclusion — set by callers (e.g. mobile receive) that must not scan
  // BOM articles. Off by default so other callers (returns, movement, …) are unaffected.
  const excludeBom = body?.excludeBom === true || body?.excludeBom === 'true'

  // Direct by-id lookup. Set by callers that already know the EXACT product the
  // user picked (e.g. mobile receive arriving from /mobile/product). Bypasses the
  // ambiguous code search + duplicate resolution so the exact clicked article is
  // returned — never a different product that happens to share a Value/SKU/UPC.
  const directProductId = body?.productId ? Number(body.productId) : null

  if (!searchTerm && !(directProductId && directProductId > 0)) {
    throw createError({
      statusCode: 400,
      statusMessage: 'Search term or productId is required'
    })
  }

  // Search by Value, SKU, UPC, or MPN. Match in tiers, exact first: raw term →
  // separators stripped ("PZN-15821889" → "PZN15821889") → prefix-less numeric
  // ("-15821889" → "15821889" matches stored "PZN15821889").
  // Quote-escaped for the OData string literals below (an apostrophe in a
  // scanned value would otherwise break every tier's filter).
  const lowerSearchTerm = odataQuote(searchTerm.toLowerCase())
  const strippedTerm = lowerSearchTerm.replace(/[\s-]+/g, '')
  // Filter by organization if provided (include org 0 = system-wide products)
  const orgClause = (filterOrgId && !includeAllOrgs) ? ` AND (AD_Org_ID eq ${Number(filterOrgId)} OR AD_Org_ID eq 0)` : ''

  // When excluding BOMs: IsBOM is the standard iDempiere flag (always present →
  // filtered server-side); the custom isJtlBom column is filtered in JS below
  // (it may not exist in every env, so it can't go in $filter).
  const bomClause = excludeBom ? ' AND (IsBOM eq false)' : ''

  const eqFilter = (term: string) =>
    `(tolower(Value) eq '${term}' OR tolower(SKU) eq '${term}' OR tolower(UPC) eq '${term}' OR tolower(mpn) eq '${term}' OR tolower(fnsku) eq '${term}') AND (isActive eq true)${bomClause}${orgClause}`

  // Tier 1: exact term — OR a direct by-id fetch when the caller already knows
  // the product. A by-id hit yields exactly one record, so the duplicate branch
  // and the lower search tiers below are skipped naturally (their guards require
  // an empty result set / a non-empty search term).
  let res: any = null
  if (directProductId && directProductId > 0) {
    const byId: any = await fetchHelper(
      event,
      `models/m_product/${directProductId}`,
      'GET',
      token,
      null
    )
    res = (byId && byId.id) ? { records: [byId] } : { records: [] }
  } else {
    res = await fetchHelper(
      event,
      `models/m_product?$filter=${string.urlEncode(eqFilter(lowerSearchTerm))}`,
      'GET',
      token,
      null
    )
  }

  // Tier 2: separators stripped
  if ((!res?.records || res.records.length === 0) && strippedTerm && strippedTerm !== lowerSearchTerm) {
    res = await fetchHelper(
      event,
      `models/m_product?$filter=${string.urlEncode(eqFilter(strippedTerm))}`,
      'GET',
      token,
      null
    )
  }

  // Tier 3: prefix-less numeric barcode — find codes ENDING WITH the number behind
  // an alphabetic-only prefix (e.g. "15821889" → "PZN15821889"). contains() on the
  // server is a superset; the post-filter enforces the exact suffix+prefix shape.
  if ((!res?.records || res.records.length === 0) && /^\d{6,}$/.test(strippedTerm)) {
    const containsFilter =
      `(contains(tolower(Value),'${strippedTerm}') OR contains(tolower(SKU),'${strippedTerm}') OR contains(tolower(UPC),'${strippedTerm}') OR contains(tolower(mpn),'${strippedTerm}') OR contains(tolower(fnsku),'${strippedTerm}')) AND (isActive eq true)${bomClause}${orgClause}`
    const sres: any = await fetchHelper(
      event,
      `models/m_product?$filter=${string.urlEncode(containsFilter)}&$top=50`,
      'GET',
      token,
      null
    )
    const n = strippedTerm.toUpperCase()
    const isSuffixMatch = (p: any) =>
      [p.Value, p.SKU, p.UPC, p.mpn, p.fnsku].some((v: any) => {
        if (!v) return false
        const u = String(v).toUpperCase().replace(/[\s-]+/g, '')
        return u.endsWith(n) && /^[A-Z]+$/.test(u.slice(0, u.length - n.length))
      })
    res = { ...(sres || {}), records: (sres?.records || []).filter(isSuffixMatch) }
  }

  // Drop JTL-BOM articles from the candidate set (custom column → post-filtered for
  // all tiers above, since it can't safely go in $filter). Standard IsBOM already excluded.
  if (!directProductId && excludeBom && res?.records?.length) {
    res = {
      ...res,
      records: res.records.filter((p: any) =>
        !((p.isJtlBom ?? p.IsJtlBom) === true || (p.isJtlBom ?? p.IsJtlBom) === 'Y'))
    }
  }

  if (res?.records && res.records.length > 0) {
    // Multiple products found — return duplicate list for user to choose.
    // Per-duplicate storage fetches run in PARALLEL (they were sequential —
    // a common code shared by many products serialized N heavy calls).
    if (res.records.length > 1) {
      const duplicates = await Promise.all(res.records.map(async (p: any) => {
        // Fetch qtyonhand for each product (split into regular + return)
        let qtyonhand = 0
        let returnQtyOnHand = 0
        try {
          const storagesRes: any = await fetchHelper(
            event,
            `models/m_storage?$filter=${string.urlEncode('M_Product_ID eq ' + p.id)}&$expand=M_Locator_ID&$top=1000`,
            'GET',
            token,
            null
          )
          const storages = Array.isArray(storagesRes?.records) ? storagesRes.records : []
          const dupOrgId = p.AD_Org_ID?.id ?? p.AD_Org_ID ?? null
          const dupReturnLocatorIds = await getReturnLocatorIds(event, token, dupOrgId)
          for (const s of storages) {
            const qty = Number(s?.QtyOnHand ?? 0)
            const locId = s?.M_Locator_ID?.id ?? s?.M_Locator_ID
            if (locId !== undefined && locId !== null && dupReturnLocatorIds.has(Number(locId))) {
              returnQtyOnHand += qty
            } else {
              qtyonhand += qty
            }
          }
        } catch (_e) { /* ignore */ }

        return {
          id: p.id,
          Name: p.Name,
          Value: p.Value,
          SKU: p.SKU,
          UPC: p.UPC,
          MPN: p.mpn || null,
          ASIN: p.asin || null,
          FNSKU: p.fnsku || null,
          isBOM: (p.IsBOM ?? p.isBOM) === true || (p.IsBOM ?? p.isBOM) === 'Y',
          isJtlBom: (p.isJtlBom ?? p.IsJtlBom) === true || (p.isJtlBom ?? p.IsJtlBom) === 'Y',
          AD_Org_ID: p.AD_Org_ID || null,
          qtyonhand: qtyonhand,
          returnQtyOnHand: returnQtyOnHand
        }
      }))

      data = {
        duplicates: duplicates,
        searchTerm: searchTerm
      }
      return data
    }

    const product = res.records[0]

    // Fetch storages for this product with locator and warehouse info expanded
    const storagesRes: any = await fetchHelper(
      event,
      `models/m_storage?$filter=${string.urlEncode('M_Product_ID eq ' + product.id)}&$expand=${string.urlEncode('M_Locator_ID,M_Warehouse')}&$top=1000`,
      'GET',
      token,
      null
    )

    const storages = Array.isArray(storagesRes?.records) ? storagesRes.records : []

    const singleProductOrgId = product.AD_Org_ID?.id ?? product.AD_Org_ID ?? null
    const returnLocatorIds = await getReturnLocatorIds(event, token, singleProductOrgId)

    // Aggregate total qty on hand and per-locator quantities. Storages sitting
    // on locators whose type has all three IsAvailableFor* flags = false are
    // tracked separately as Return Qty On Hand.
    let qtyonhand = 0
    let returnQtyOnHand = 0
    const locatorMap: Record<string, { id: any, code: string, qtyOnHand: number, warehouseId?: any, orgId?: any, x?: string, y?: string, z?: string, isReturn?: boolean }> = {}
    let warehouseIdFromStorage = null

    for (const s of storages) {
      const qty = Number(s?.QtyOnHand ?? 0)
      if (!qty) continue
      const locObj = s?.M_Locator_ID || s?.m_locator_id || {}
      const locId = locObj?.id ?? String(locObj) ?? 'unknown'
      const isReturn = locId !== undefined && locId !== null && returnLocatorIds.has(Number(locId))
      if (isReturn) {
        returnQtyOnHand += qty
      } else {
        qtyonhand += qty
      }
      const locX = locObj?.X
      const locY = locObj?.Y
      const locZ = locObj?.Z
      const locCode = (locX || locY || locZ)
        ? [locX, locY, locZ].filter(Boolean).join(' - ')
        : locObj?.Value || locObj?.identifier || locObj?.Name || locObj?.value || String(locId)

      // Extract warehouse ID from M_Warehouse array
      const warehouseArray = s?.M_Warehouse || s?.m_warehouse || []
      const locWarehouseId = Array.isArray(warehouseArray) && warehouseArray.length > 0
        ? warehouseArray[0]?.id
        : null

      // Extract organization ID from expanded locator
      const locOrgId = locObj?.AD_Org_ID?.id || locObj?.AD_Org_ID || null

      // Capture warehouse ID from first locator with inventory
      if (!warehouseIdFromStorage && locWarehouseId) {
        warehouseIdFromStorage = locWarehouseId
      }

      if (!locatorMap[locId]) {
        locatorMap[locId] = { id: locId, code: locCode, qtyOnHand: 0, warehouseId: locWarehouseId, orgId: locOrgId, x: locX, y: locY, z: locZ, isReturn }
      }
      locatorMap[locId].qtyOnHand += qty
    }

    const productOrgId = product.AD_Org_ID?.id || product.AD_Org_ID
    const warehouseId = body?.warehouseId

    let locators = Object.values(locatorMap)
      .filter(l => l.qtyOnHand > 0 && (includeAllOrgs || !l.orgId || l.orgId === productOrgId))
      .sort((a, b) => b.qtyOnHand - a.qtyOnHand)

    // Fetch locators for the product's organization in the warehouse
    let availableLocators: any[] = []
    let orgWarehouseIds: any[] = []

    console.log('[product-lookup] warehouseId:', warehouseId, 'productOrgId:', productOrgId)

    if (warehouseId || productOrgId) {
      // Find warehouses that belong to the product's org
      // This ensures we only show locators from correctly owned warehouses
      let locatorFilter = 'IsActive eq true'

      if (productOrgId) {
        const whRes: any = await fetchHelper(
          event,
          `models/m_warehouse?$filter=${string.urlEncode('AD_Org_ID eq ' + productOrgId + ' AND IsActive eq true')}`,
          'GET',
          token,
          null
        )
        orgWarehouseIds = (whRes?.records || []).map((w: any) => w.id).filter(Boolean)
        console.log('[product-lookup] Warehouses for org', productOrgId, ':', orgWarehouseIds)
      }

      if (orgWarehouseIds.length > 0) {
        // Use warehouses that belong to the product's org
        const whFilterParts = orgWarehouseIds.map((id: any) => 'M_Warehouse_ID eq ' + id)
        locatorFilter = '(' + whFilterParts.join(' OR ') + ') AND AD_Org_ID eq ' + productOrgId + ' AND ' + locatorFilter
      } else if (productOrgId) {
        // Fallback: org-only filter
        locatorFilter = 'AD_Org_ID eq ' + productOrgId + ' AND ' + locatorFilter
      } else if (warehouseId) {
        // No org available, use session warehouse
        locatorFilter = 'M_Warehouse_ID eq ' + warehouseId + ' AND ' + locatorFilter
      }

      let locatorsRes: any = await fetchHelper(
        event,
        `models/m_locator?$filter=${string.urlEncode(locatorFilter)}&$orderby=${string.urlEncode('IsDefault desc, PriorityNo desc')}`,
        'GET',
        token,
        null
      )

      console.log('[product-lookup] Locators for product org (with warehouse):', locatorsRes?.records?.length || 0)

      // If no locators found with warehouse filter and product org differs, try without warehouse filter
      if ((!locatorsRes?.records || locatorsRes.records.length === 0) && productOrgId) {
        console.log('[product-lookup] Retrying locator search with only org filter for orgId:', productOrgId)
        locatorsRes = await fetchHelper(
          event,
          `models/m_locator?$filter=${string.urlEncode('AD_Org_ID eq ' + productOrgId + ' AND IsActive eq true')}&$orderby=${string.urlEncode('IsDefault desc, PriorityNo desc')}`,
          'GET',
          token,
          null
        )
        console.log('[product-lookup] Locators for product org (org only):', locatorsRes?.records?.length || 0)
      }

      if (locatorsRes?.records && locatorsRes.records.length > 0) {
        // Carry the REAL on-hand qty per locator (from the storage aggregation
        // above) so the client can show/select locators with their available qty.
        availableLocators = locatorsRes.records.map((loc: any) => ({
          id: loc.id,
          code: (loc.X || loc.Y || loc.Z) ? [loc.X, loc.Y, loc.Z].filter(Boolean).join(' - ') : (loc.Value || loc.identifier || loc.Name),
          qtyOnHand: locatorMap[loc.id]?.qtyOnHand || 0,
          isDefault: loc.IsDefault || false,
          warehouseId: loc.M_Warehouse_ID?.id || loc.M_Warehouse_ID,
          orgId: loc.AD_Org_ID?.id || loc.AD_Org_ID
        }))
        // If no locators with qty, use the default locator or first locator as initial
        if (locators.length === 0) {
          const defaultLocator = availableLocators.find(l => l.isDefault) || availableLocators[0]
          if (defaultLocator) {
            locators = [defaultLocator]
          }
        }
      }
    }

    // Extract warehouse ID - prioritize from org's warehouses, then availableLocators, then session fallback
    // Use the org's own warehouse (not warehouseIdFromStorage which may be wrong)
    let warehouseIdForProduct = orgWarehouseIds.length > 0 ? orgWarehouseIds[0] : null
    if (!warehouseIdForProduct && availableLocators.length > 0 && availableLocators[0].warehouseId) {
      warehouseIdForProduct = availableLocators[0].warehouseId
    }
    if (!warehouseIdForProduct) {
      warehouseIdForProduct = body?.warehouseId
    }

    data = {
      id: product.id,
      Name: product.Name,
      Value: product.Value,
      SKU: product.SKU,
      UPC: product.UPC,
      MPN: product.mpn || null,
      ASIN: product.asin || null,
      FNSKU: product.fnsku || null,
      isBOM: (product.IsBOM ?? product.isBOM) === true || (product.IsBOM ?? product.isBOM) === 'Y',
      isJtlBom: (product.isJtlBom ?? product.IsJtlBom) === true || (product.isJtlBom ?? product.IsJtlBom) === 'Y',
      Description: product.Description,
      ImageURL: product.ImageURL || null,
      labelPrinterId: product.labelprinter_rma?.id || null,
      labelPrinter: product.labelprinter_rma?.identifier || null,
      qtyonhand: qtyonhand,
      returnQtyOnHand: returnQtyOnHand,
      qtyreserved: product.QtyReserved || 0,
      qtyavailable: product.QtyAvailable || 0,
      qtyordered: product.QtyOrdered || 0, // Sales orders (customer orders)
      qtypurchased: product.QtyOrdered_PO || 0, // Purchase orders
      Weight: product.Weight || 0,
      ShelfWidth: product.ShelfWidth || 0,
      ShelfHeight: product.ShelfHeight || 0,
      ShelfDepth: product.ShelfDepth || 0,
      Strapi_Product_documentId: product.Strapi_Product_documentId || null,
      AD_Org_ID: product.AD_Org_ID || null,
      M_Warehouse_ID: warehouseIdForProduct,
      locators: locators,
      availableLocators: availableLocators
    }
  } else {
    throw createError({
      statusCode: 404,
      statusMessage: 'Product not found'
    })
  }

  return data
}

export default defineEventHandler(async (event) => {
  // Auth-only retry: business errors (404 "Product not found" etc.) pass
  // through without the former blanket token refresh — see withAuthRetry.
  return await withAuthRetry(event, handleFunc)
})
