package com.ffii.fpsms.m18.service

import com.ffii.core.utils.JwtTokenUtil
import com.ffii.fpsms.api.service.ApiCallerService
import com.ffii.fpsms.m18.M18Config
import com.ffii.fpsms.m18.model.*
import com.ffii.fpsms.m18.utils.CommonUtils
import com.ffii.fpsms.m18.web.models.M18CommonRequest
import com.ffii.fpsms.modules.master.entity.UomConversion
import com.ffii.fpsms.modules.master.enums.ShopType
import com.ffii.fpsms.modules.master.service.*
import com.ffii.fpsms.modules.master.web.models.*
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.math.BigDecimal
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import com.ffii.fpsms.m18.model.SyncResult
import com.ffii.fpsms.modules.master.entity.Items
import java.time.Instant
import java.time.LocalDate
import java.time.ZoneId

@Service
open class M18MasterDataService(
    val m18Config: M18Config,
    val apiCallerService: ApiCallerService,
    val itemsService: ItemsService,
    val shopService: ShopService,
    val uomConversionService: UomConversionService,
    val currencyService: CurrencyService,
    val itemUomService: ItemUomService,
    val bomService: BomService,
    val bomMaterialService: BomMaterialService,
    val m18CunitService: M18CunitService,
) {

    val logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java)
    val commonUtils = CommonUtils()
    val formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
    private val one = BigDecimal.ONE
    private val ratio2268 = BigDecimal("2268")
    private val ratio5 = BigDecimal("5")

    // M18 Conditions
//    val lastModifyDate = LocalDate.now().minusDays(1)
//    val lastModifyDateConds = "lastModifyDate=largerThan=$lastModifyDate"
    val seriesIdList =
        listOf(m18Config.SERIESID_SC, m18Config.SERIESID_SE, m18Config.SERIESID_SF, m18Config.SERIESID_SR)
    val seriesIdConds =
        "(" + commonUtils.listToString(seriesIdList.filterNotNull(), "seriesId=unequal=", "=or=") + ")"
    val beIdList = listOf(m18Config.BEID_PF, m18Config.BEID_PP, m18Config.BEID_TOA)
    val beIdConds = "(" + commonUtils.listToString(beIdList.filterNotNull(), "beId=equal=", "=or=") + ")"
//    val beIdConds = commonUtils.BEID_CONDS

    // M18 API
    val M18_COMMON_FETCH_LIST_API = "/search/search"
    val M18_COMMON_LOAD_LINE_API = "/root/api/read"

    val M18_LOAD_PRODUCT_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.PRODUCT.value}"
    val M18_LOAD_VENDOR_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.VENDOR.value}"
    val M18_LOAD_UNIT_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.UNIT.value}"
    val M18_LOAD_CURRENCY_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.CURRENCY.value}"
    val M18_LOAD_BOM_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.BOM.value}"
    val M18_LOAD_BUSINESS_UNIT_API = "${M18_COMMON_LOAD_LINE_API}/${StSearchType.BUSINESS_UNIT.value}" // for shop po?

    private fun isBothOne(ratioN: BigDecimal?, ratioD: BigDecimal?): Boolean {
        return ratioN?.compareTo(one) == 0 && ratioD?.compareTo(one) == 0
    }

    private fun isPair2268And5(ratioN: BigDecimal?, ratioD: BigDecimal?): Boolean {
        return ratioN?.compareTo(ratio2268) == 0 && ratioD?.compareTo(ratio5) == 0
    }

    /**
     * Problematic ratio cases that should be corrected by m18_cunit:
     * 1) uomId=4 and ratioN/ratioD are not both 1
     * 2) uomId=785 and ratioN/ratioD do not contain 453
     */
    private fun shouldSyncRatioFromCunit(unitId: Long, ratioN: BigDecimal?, ratioD: BigDecimal?): Boolean {
        val caseUom4 = unitId == 4L && !isBothOne(ratioN, ratioD)
        val caseUom785 = unitId == 785L && !isPair2268And5(ratioN, ratioD)
        return caseUom4 || caseUom785
    }

    /**
     * Same rule as [shouldSyncRatioFromCunit], but based on local uom_conversion.id.
     */
    private fun shouldSyncRatioFromCunitByLocalUomId(localUomId: Long?, ratioN: BigDecimal?, ratioD: BigDecimal?): Boolean {
        if (localUomId == null) return false
        val caseUom4 = localUomId == 4L && !isBothOne(ratioN, ratioD)
        val caseUom785 = localUomId == 785L && !isPair2268And5(ratioN, ratioD)
        return caseUom4 || caseUom785
    }

    /**
     * Safety net: when m18_cunit has no rows, force a full unit sync first so cunit ratios are available.
     */
    open fun ensureCunitSeededForAllIfEmpty() {
        val cunitRows = m18CunitService.countActiveRows()
        if (cunitRows > 0L) return
        logger.warn("m18_cunit has 0 rows; triggering full unit sync first to seed cunit data for all units.")
        val result = saveUnits(M18CommonRequest())
        logger.warn(
            "m18_cunit seed sync finished: processed=${result.totalProcessed}, " +
                "success=${result.totalSuccess}, fail=${result.totalFail}"
        )
    }

    // --------------------------------------------- Common Function --------------------------------------------- ///
    private inline fun <reified T : Any> getList(
        stSearch: String?,
        params: String? = null,
        conds: String? = null,
        request: M18CommonRequest,
    ): T? {
        val lastModifyDateFromConds = request.modifiedDateFrom?.let { "lastModifyDate=largerOrEqual=${it}" }
        val lastModifyDateToConds = request.modifiedDateTo?.let{ "lastModifyDate=lessOrEqual=${it}" }
        val haveFromAndTo = lastModifyDateFromConds != null && lastModifyDateToConds != null
        val finalConds = if (lastModifyDateFromConds == null && lastModifyDateToConds == null) {
            conds
        } else {
            conds + "=and=(${lastModifyDateFromConds ?: ""}${if(haveFromAndTo) "=and=" else ""}${lastModifyDateToConds ?: ""})"
        }

        val request = M18CommonListRequest(
            stSearch = stSearch,
            params = params,
            conds = finalConds
        )

        val response = apiCallerService.get<T, M18CommonListRequest>(
            M18_COMMON_FETCH_LIST_API,
            request
        ).block()

        return response
    }

    private inline fun <reified T : Any> getLine(
        id: Long,
        params: String?,
        api: String,
    ): T? {
        val request = M18CommonLineRequest(
            id = id,
            params = params,
        )

        val response = apiCallerService.get<T, M18CommonLineRequest>(
            api,
            request
        ).block()

        return response
    }

    // --------------------------------------------- Product --------------------------------------------- ///
    open fun getProducts(request: M18CommonRequest): M18ProductListResponse? {
        // seems no beId
        return getList<M18ProductListResponse>(
            stSearch = StSearchType.PRODUCT.value,
            params = null,
            conds = seriesIdConds,
            request = request
        )
//        val itemsParams = M18CommonListRequest(
//            stSearch = StSearchType.PRODUCT.value,
//            params = null,
//            conds = seriesIdConds
//        )
//
//        val items = apiCallerService.get<M18ProductListResponse, M18CommonListRequest>(
//            M18_COMMON_FETCH_LIST_API,
//            itemsParams
//        ).block()
//
//        return items
    }

    open fun getProduct(id: Long): M18ProductResponse? {
        logger.info("M18 Product ID: $id")
        return getLine<M18ProductResponse>(
            id = id,
            params = null,
            api = M18_LOAD_PRODUCT_API
        )
    }

    open fun saveProduct(id: Long): MessageResponse? {
        try {
            ensureCunitSeededForAllIfEmpty()
            val itemDetail = getProduct(id)
            val pro = itemDetail?.data?.pro?.get(0)
            val price = itemDetail?.data?.price

            if (itemDetail != null && pro != null) {
                val existingItem = itemsService.findByM18Id(id)
                val saveItemRequest = NewItemRequest(
                    code = pro.code,
                    name = pro.desc,
//                            type = if (pro.seriesId == m18Config.SERIESID_PF) ProductType.MATERIAL
//                            else ItemType.PRODUCT,
                    type = when (pro.udfProducttype) {
                        M18ItemType.CONSUMABLES.type -> ItemType.CONSUMABLES.type
                        M18ItemType.NONCONSUMABLES.type -> ItemType.NONCONSUMABLES.type
                        M18ItemType.FG.type -> ItemType.FG.type
                        M18ItemType.SFG.type -> ItemType.SFG.type
                        M18ItemType.ITEM.type -> ItemType.ITEM.type
                        else -> ItemType.MATERIAL.type
                    },
                    id = existingItem?.id,
                    description = pro.desc,
                    remarks = null,
                    shelfLife = null,
                    countryOfOrigin = null,
                    maxQty = null,
                    m18Id = id,
                    m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate),
                    qcCategoryId = null,
                    store_id = null,
                    warehouse = null,
                    area = null,
                    slot = null,
                    LocationCode = null,
                    isEgg = null,
                    isFee = null,
                    isBag = null,
                    qcType = null,
                )

                val savedItem = itemsService.saveItem(saveItemRequest)
                logger.info("Processing item uom...")
                // Find the item uom that ready to delete (not in m18)
                val existingItemUoms = savedItem.id?.let { itemUomService.findAllByItemsId(it) }
                val m18ItemUomIds = price?.map { it.id } ?: listOf()

                // Delete the item uom
                logger.info("Deleting item uom...")
//                        logger.info("Item Uom: ${existingItemUoms?.map { it.m18Id }}")
//                        logger.info("M18: ${m18ItemUomIds}")
                existingItemUoms?.filter { it.m18Id !in m18ItemUomIds }?.mapNotNull { it.id }
                    ?.let { itemUomService.deleteItemUoms(it) }

                // Update the item uom
                logger.info("Updating item uom...")
                val hasBaseUom4 = price?.any { p ->
                    p.basicUnit && (uomConversionService.findByM18Id(p.unitId)?.id == 4L)
                } == true
                val forceCunitForAllRows = hasBaseUom4 && (price?.any { p ->
                    val localBaseId = uomConversionService.findByM18Id(p.unitId)?.id
                    shouldSyncRatioFromCunitByLocalUomId(localBaseId, p.ratioN, p.ratioD)
                } == true)
                price?.forEach {
                    val endMillis = it.endDate
                    val endInstant = Instant.ofEpochMilli(endMillis)
                    val now = Instant.now()
                    val localUomId = uomConversionService.findByM18Id(it.unitId)?.id
                    val keepOriginalItemRatio = forceCunitForAllRows
                    val useUnitRatios = forceCunitForAllRows
                    val unitRatios = if (useUnitRatios) m18CunitService.resolveRatiosByM18UnitId(it.unitId) else null
                    logger.info(
                        "ItemUom ratio apply (single): itemM18Id=$id m18UomId=${it.unitId} localUomId=$localUomId " +
                            "hasBaseUom4=$hasBaseUom4 forceCunitForAllRows=$forceCunitForAllRows useCunit=$useUnitRatios oldRatioN=${it.ratioN} oldRatioD=${it.ratioD} " +
                            "cunitRatioN=${unitRatios?.ratioN} cunitRatioD=${unitRatios?.ratioD}"
                    )
                    val itemUomRequest = ItemUomRequest(
                        m18UomId = it.unitId,
                        itemId = savedItem.id,
                        baseUnit = it.basicUnit,
                        stockUnit = it.stkUnit,
                        pickingUnit = it.pickUnit,
                        salesUnit = it.saleUnit,
                        purchaseUnit = it.purUnit,
                        price = null,
                        currencyId = null,
                        m18Id = it.id,
                        m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate),
                        ratioD = if (useUnitRatios) (unitRatios?.ratioD ?: it.ratioD) else it.ratioD,
                        ratioN = if (useUnitRatios) (unitRatios?.ratioN ?: it.ratioN) else it.ratioN,
                        itemRatioD = if (keepOriginalItemRatio) it.ratioD else null,
                        itemRatioN = if (keepOriginalItemRatio) it.ratioN else null,
                        deleted = it.expired || endInstant.isBefore(now)
                    )

//                            logger.info("saved item id: ${savedItem.id}")
                    itemUomService.saveItemUom(itemUomRequest)
                }

                logger.info("Success (M18 Item): ${id} | ${pro.code} | ${pro.desc}")
                return savedItem
            } else {
                logger.error("Fail Message: ${itemDetail?.messages?.get(0)?.msgDetail}")
                logger.error("Fail: Item ID - ${id} Not Found")
                return null
            }
        } catch (e: Exception) {
            logger.error("Exception")
            logger.error("Fail Message: ${e.message}")
            logger.error("Fail: Item ID - ${id}")
            return null
        }
    }

    /** Sync one product/material from M18 by item code (search list, then load line — same idea as PO/DO by code). */
    open fun saveProductByCode(code: String): SyncResult {
        val trimmed = code.trim()
        if (trimmed.isEmpty()) {
            return SyncResult(totalProcessed = 1, totalSuccess = 0, totalFail = 1, query = "empty code")
        }
        ensureCunitSeededForAllIfEmpty()
        val fromLocal = itemsService.findByCode(trimmed)?.m18Id
        val m18Id = fromLocal ?: run {
            val conds = "(code=equal=$trimmed)"
            val listResponse = try {
                getList<M18ProductListResponse>(
                    stSearch = StSearchType.PRODUCT.value,
                    params = null,
                    conds = conds,
                    request = M18CommonRequest(),
                )
            } catch (e: Exception) {
                logger.error("(saveProductByCode) M18 search failed: ${e.message}", e)
                null
            }
            listResponse?.values?.firstOrNull()?.id
        }
        if (m18Id == null) {
            return SyncResult(
                totalProcessed = 1,
                totalSuccess = 0,
                totalFail = 1,
                query = "code=equal=$trimmed",
            )
        }
        val result = saveProduct(m18Id)
        return if (result != null) {
            SyncResult(totalProcessed = 1, totalSuccess = 1, totalFail = 0, query = "code=equal=$trimmed")
        } else {
            SyncResult(totalProcessed = 1, totalSuccess = 0, totalFail = 1, query = "code=equal=$trimmed")
        }
    }

    open fun saveProducts(request: M18CommonRequest): SyncResult {
        logger.info("--------------------------------------------Start - Saving M18 Products / Materials--------------------------------------------")
        ensureCunitSeededForAllIfEmpty()

        val items = getProducts(request)
        val exampleProducts = listOf<Long>(10946L, 3825L)

        // ── New: cache for findByM18Id ─────────────────────────────────────────
        // M18 item.id → internal Item entity (or just the id if you only need id)
        val itemCache = mutableMapOf<Long, Items?>()

        val successList = mutableListOf<Long>()
        val failList = mutableListOf<Long>()

        val values = items?.values?.sortedBy { it.id }
        if (values != null) {
            values.forEach { item ->
                // if (item.id in exampleProducts) {    // keep your debug filter if needed

                try {
                    val itemDetail = getProduct(item.id)
                    val pro = itemDetail?.data?.pro?.get(0)
                    val price = itemDetail?.data?.price

                    if (itemDetail != null && pro != null) {
                        // ── Use cache instead of direct call ────────────────────────
                        val existingItem = itemCache.getOrPut(item.id) {
                            itemsService.findByM18Id(item.id)
                        }

                        val saveItemRequest = NewItemRequest(
                            code = pro.code,
                            name = pro.desc,
                            type = when (pro.udfProducttype) {
                                M18ItemType.CONSUMABLES.type -> ItemType.CONSUMABLES.type
                                M18ItemType.NONCONSUMABLES.type -> ItemType.NONCONSUMABLES.type
                                M18ItemType.FG.type -> ItemType.FG.type
                                M18ItemType.SFG.type -> ItemType.SFG.type
                                M18ItemType.ITEM.type -> ItemType.ITEM.type
                                else -> ItemType.MATERIAL.type
                            },
                            id = existingItem?.id,
                            description = pro.desc,
                            remarks = null,
                            shelfLife = null,
                            countryOfOrigin = null,
                            maxQty = null,
                            m18Id = item.id,
                            m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate),
                            qcCategoryId = null,
                            store_id = null,
                            warehouse = null,
                            area = null,
                            slot = null,
                            LocationCode = null,
                            isEgg = null,
                            isFee = null,
                            isBag = null,
                            qcType = null
                        )

                        val savedItem = itemsService.saveItem(saveItemRequest)
                        logger.info("Processing item uom...")

                        // Optional: cache findAllByItemsId if you think it might be called multiple times
                        // (usually not needed here because each savedItem.id is unique)
                        val existingItemUoms = savedItem.id?.let { itemUomService.findAllByItemsId(it) }

                        val m18ItemUomIds = price?.map { it.id } ?: listOf()

                        // Delete old UOMs not present in M18
                        logger.info("Deleting item uom...")
                        existingItemUoms?.filter { it.m18Id !in m18ItemUomIds }?.mapNotNull { it.id }
                            ?.let { itemUomService.deleteItemUoms(it) }

                        // Update / create UOMs from M18
                        logger.info("Updating item uom...")
                        val hasBaseUom4 = price?.any { p ->
                            p.basicUnit && (uomConversionService.findByM18Id(p.unitId)?.id == 4L)
                        } == true
                        val forceCunitForAllRows = hasBaseUom4 && (price?.any { p ->
                            val localBaseId = uomConversionService.findByM18Id(p.unitId)?.id
                            shouldSyncRatioFromCunitByLocalUomId(localBaseId, p.ratioN, p.ratioD)
                        } == true)
                        price?.forEach {
                            val endMillis = it.endDate
                            val endInstant = Instant.ofEpochMilli(endMillis)
                            val now = Instant.now()
                            val localUomId = uomConversionService.findByM18Id(it.unitId)?.id
                            val keepOriginalItemRatio = forceCunitForAllRows
                            val useUnitRatios = forceCunitForAllRows
                            val unitRatios = if (useUnitRatios) m18CunitService.resolveRatiosByM18UnitId(it.unitId) else null
                            logger.info(
                                "ItemUom ratio apply (bulk): itemM18Id=${item.id} m18UomId=${it.unitId} localUomId=$localUomId " +
                                    "hasBaseUom4=$hasBaseUom4 forceCunitForAllRows=$forceCunitForAllRows useCunit=$useUnitRatios oldRatioN=${it.ratioN} oldRatioD=${it.ratioD} " +
                                    "cunitRatioN=${unitRatios?.ratioN} cunitRatioD=${unitRatios?.ratioD}"
                            )

                            val itemUomRequest = ItemUomRequest(
                                m18UomId = it.unitId,
                                itemId = savedItem.id,
                                baseUnit = it.basicUnit,
                                stockUnit = it.stkUnit,
                                pickingUnit = it.pickUnit,
                                salesUnit = it.saleUnit,
                                purchaseUnit = it.purUnit,
                                price = null,
                                currencyId = null,
                                m18Id = it.id,
                                m18LastModifyDate = commonUtils.timestampToLocalDateTime(pro.lastModifyDate),
                                ratioD = if (useUnitRatios) (unitRatios?.ratioD ?: it.ratioD) else it.ratioD,
                                ratioN = if (useUnitRatios) (unitRatios?.ratioN ?: it.ratioN) else it.ratioN,
                                itemRatioD = if (keepOriginalItemRatio) it.ratioD else null,
                                itemRatioN = if (keepOriginalItemRatio) it.ratioN else null,
                                deleted = it.expired || endInstant.isBefore(now)
                            )

                            itemUomService.saveItemUom(itemUomRequest)
                        }

                        successList.add(item.id)
                        logger.info("Success Count ${successList.size}: ${item.id} | ${pro.code} | ${pro.desc}")
                    } else {
                        failList.add(item.id)
                        logger.error("Fail Message: ${itemDetail?.messages?.get(0)?.msgDetail}")
                        logger.error("Fail Count ${failList.size}: Item ID - ${item.id} Not Found")
                    }
                } catch (e: Exception) {
                    failList.add(item.id)
                    logger.error("Exception")
                    logger.error("Fail Message: ${e.message}")
                    logger.error("Fail Count ${failList.size}: Item ID - ${item.id}")
                }
                // }   // end of exampleProducts filter
            }
        } else {
            logger.error("Items List is null. May occur errors.")
        }

        logger.info("Total Success (${successList.size})")
        if (failList.size > 0) {
            logger.error("Total Fail (${failList.size}): $failList")
        }

        logger.info("--------------------------------------------End - Saving M18 Products / Materials--------------------------------------------")

        return SyncResult(
            totalProcessed = successList.size + failList.size,
            totalSuccess = successList.size,
            totalFail = failList.size
        )
    }

    /**
     * Targeted re-sync for locally identified problematic item_uom ratios:
     * - uomId = 4 and ratioN != 1
     * - uomId = 785 and ratioN != 453
     *
     * Uses local item_uom + item.m18Id to fetch only affected products from M18 and re-save their item_uom.
     */
    open fun resyncProblemItemUomsFromM18(): SyncResult {
        logger.info("--------------------------------------------Start - Re-sync Problem Item UOMs--------------------------------------------")
        val m18Ids = itemUomService.findDistinctProblemItemM18IdsForUomResync().distinct().sorted()
        val successList = mutableListOf<Long>()
        val failList = mutableListOf<Long>()
        logger.warn("Problem item_uom selector found ${m18Ids.size} item(s) for re-sync.")
        if (m18Ids.isNotEmpty()) {
            logger.warn("Problem item m18Id sample (up to 20): ${m18Ids.take(20)}")
        }

        m18Ids.forEach { m18ItemId ->
            try {
                logger.warn("Re-sync item_uom: calling M18 product API for id=$m18ItemId")
                val result = saveProduct(m18ItemId)
                if (result != null) {
                    successList.add(m18ItemId)
                    logger.warn("Re-sync item_uom: M18 API sync success for id=$m18ItemId")
                } else {
                    failList.add(m18ItemId)
                    logger.warn("Re-sync item_uom: M18 API sync returned null for id=$m18ItemId")
                }
            } catch (e: Exception) {
                failList.add(m18ItemId)
                logger.error("Re-sync problem item uom failed for m18ItemId=$m18ItemId: ${e.message}", e)
            }
        }

        logger.info("Problem item_uom re-sync done. total=${m18Ids.size}, success=${successList.size}, fail=${failList.size}")
        logger.info("--------------------------------------------End - Re-sync Problem Item UOMs--------------------------------------------")
        return SyncResult(
            totalProcessed = m18Ids.size,
            totalSuccess = successList.size,
            totalFail = failList.size
        )
    }

    // --------------------------------------------- Vendor --------------------------------------------- ///
    open fun getVendors(request: M18CommonRequest): M18VendorListResponse? {
        return getList<M18VendorListResponse>(
            stSearch = StSearchType.VENDOR.value,
            params = null,
            conds = beIdConds,
            request = request
        )
    }

    open fun getVendor(id: Long): M18VendorResponse? {
        logger.info("M18 Vendor ID: $id")
        return getLine<M18VendorResponse>(
            id = id,
            params = null,
            api = M18_LOAD_VENDOR_API
        )
    }

    open fun saveVendors(request: M18CommonRequest) : SyncResult{
        logger.info("--------------------------------------------Start - Saving M18 Vendors--------------------------------------------")
        val vendors = getVendors(request)
        val exampleVendors = listOf<Long>(191L)

        val successList = mutableListOf<Long>()
        val failList = mutableListOf<Long>()
        val values = vendors?.values?.sortedBy { it.id }

        if (values != null) {
            values.forEach { vendor ->
//                if (vendor.id in exampleVendors) {
                try {
                    val vendorDetail = getVendor(vendor.id)

                    if (vendorDetail != null && vendorDetail.data?.ven != null) {
                        val ven = vendorDetail.data.ven[0]

                        val saveShopRequest = SaveShopRequest(
                            id = null,
                            code = ven.code,
                            name = ven.descZhTW.ifEmpty { ven.descZhCN.ifEmpty { ven.desc } },
                            brNo = null,
                            contactNo = ven.tel,
                            contactEmail = ven.email,
                            contactName = null,
                            addr1 = ven.ad1,
                            addr2 = ven.ad2,
                            addr3 = ven.ad3,
                            addr4 = ven.ad4,
                            district = null,
                            type = ShopType.SUPPLIER.value,
                            m18Id = vendor.id,
                            m18LastModifyDate = commonUtils.timestampToLocalDateTime(ven.lastModifyDate)
                        )

                        shopService.saveShop(saveShopRequest)
                        successList.add(vendor.id)
                        logger.info("Success Count ${successList.size}: ${vendor.id} | ${ven.code} | ${ven.descZhTW.ifEmpty { ven.descZhCN.ifEmpty { ven.desc } }}")
                    } else {
                        failList.add(vendor.id)
                        logger.error("Fail Message: ${vendorDetail?.messages?.get(0)?.msgDetail}")
                        logger.error("Fail Count ${failList.size}: Vendor ID - ${vendor.id} Not Found")
                    }
                } catch (e: Exception) {
                    failList.add(vendor.id)
                    logger.error("Exception")
                    logger.error("Fail Message: ${e.message}")
                    logger.error("Fail Count ${failList.size}: Vendor ID - ${vendor.id}")
                }
//                }
            }
        } else {
            logger.error("Vendor List is null. May occur errors.")
        }

        logger.info("Total Success (${successList.size})")

        if (failList.size > 0) {
            logger.error("Total Fail (${failList.size}): $failList")
        }
        logger.info("--------------------------------------------End - Saving M18 Vendors--------------------------------------------")

        return SyncResult(
            totalProcessed = successList.size + failList.size,
            totalSuccess = successList.size,
            totalFail = failList.size
        )
    }

    // --------------------------------------------- Unit (UoM) --------------------------------------------- ///
    open fun getUnits(request: M18CommonRequest): M18UnitListResponse? {
        // seems no beId
        return getList<M18UnitListResponse>(
            stSearch = StSearchType.UNIT.value,
            params = null,
            conds = null,
            request = request
        )
    }

    open fun getUnit(id: Long): M18UnitResponse? {
        logger.info("M18 Unit ID: $id")
        return getLine<M18UnitResponse>(
            id = id,
            params = null,
            api = M18_LOAD_UNIT_API
        )
    }

    open fun saveUnits(request: M18CommonRequest): SyncResult {
        logger.info("--------------------------------------------Start - Saving M18 Units--------------------------------------------")
        val units = getUnits(request)

        val successTransformList = mutableListOf<Long>()
        val successSaveList = mutableListOf<Long>()
        val failTransformList = mutableListOf<Long>()
        val failSaveList = mutableListOf<Long>()

        val values = units?.values?.sortedBy { it.id }
        if (values != null) {
            val finalUnitList = arrayListOf<UomConversion>()

            // transform unit
            values.forEach { value ->
                try {
                    val unitDetail = getUnit(value.id)

                    if (unitDetail != null && unitDetail.data?.unit != null) {
                        val unit = unitDetail.data.unit[0]
                        try {
                            m18CunitService.replaceForUnit(unit.id, unitDetail.data!!)
                        } catch (e: Exception) {
                            logger.error("M18 cunit save failed for unit ${unit.id}: ${e.message}", e)
                        }
                        val tempObject = UomConversionService.BomObject().apply {
                            code = unit.code
                            udfudesc = unit.udfudesc
                            udfShortDesc = unit.udfShortDesc
                            lastModifyDate = commonUtils.timestampToLocalDateTime(unit.lastModifyDate).format(formatter)
                            id = unit.id
                        }
                        finalUnitList += uomConversionService.transformItem(tempObject)
                        successTransformList += unit.id
                        logger.info("Transform Success (M18): ${unit.id}")
                    } else {
                        failTransformList.add(value.id)
                        logger.error("Fail Message: ${unitDetail?.messages?.get(0)?.msgDetail}")
                        logger.error("Fail Count ${failTransformList.size}: Unit ID - ${value.id} Not Found")
                    }
                } catch (e: Exception) {
                    failTransformList.add(value.id)
                    logger.error("Transform Exception")
                    logger.error("Transform Fail Message: ${e.message}")
                    logger.error("Transform Fail Count ${failTransformList.size}: Unit ID - ${value.id}")
                }
            }

            uomConversionService.calculateSizeInGram(finalUnitList)

            finalUnitList.forEach {
                try {
                    uomConversionService.saveUomConversion(it)
                    successSaveList += it.m18Id
                    logger.info("Save Success (M18): ${it.m18Id}")
                } catch (e: Exception) {
                    failSaveList.add(it.m18Id)
                    logger.error("Save Exception")
                    logger.error("Save Fail Message: ${e.message}")
                    logger.error("Save Fail Count ${failTransformList.size}: Unit ID - ${it.m18Id}")
                }
            }
        } else {
            logger.error("Unit List is null. May occur errors.")
        }

        logger.info("Total Transform Success (${successTransformList.size})")
        logger.info("Total Save Success (${successSaveList.size})")

        if (failTransformList.size > 0) {
            logger.error("Total Transform Fail (${failTransformList.size}): $failTransformList")
        }

        if (failSaveList.size > 0) {
            logger.error("Total Save Fail (${failSaveList.size}): $failSaveList")
        }
        logger.info("--------------------------------------------End - Saving M18 Units--------------------------------------------")
        val processed = values?.size ?: 0
        val failed = failTransformList.size + failSaveList.size
        return SyncResult(
            totalProcessed = processed,
            totalSuccess = successSaveList.size,
            totalFail = failed,
            query = "stSearch=${StSearchType.UNIT.value}",
        )
    }

    // --------------------------------------------- Currency --------------------------------------------- ///
    open fun getCurrencies(request: M18CommonRequest): M18CurrencyListResponse? {
        return getList<M18CurrencyListResponse>(
            stSearch = StSearchType.CURRENCY.value,
            params = null,
            conds = null,
            request = request
        )
    }

    open fun getCurrency(id: Long): M18CurrencyResponse? {
        logger.info("M18 Currency ID: $id")
        return getLine<M18CurrencyResponse>(
            id = id,
            params = null,
            api = M18_LOAD_CURRENCY_API
        )
    }

    open fun saveCurrencies(request: M18CommonRequest) : SyncResult{
        logger.info("--------------------------------------------Start - Saving M18 Currencies--------------------------------------------")
        val currencies = getCurrencies(request)

        val successList = mutableListOf<Long>()
        val failList = mutableListOf<Long>()

        val values = currencies?.values?.sortedBy { it.id }
        if (values != null) {
            // save currency
            values.forEach { currency ->
                try {
                    val currencyRequest = SaveCurrencyRequest(
                        id = null,
                        code = currency.code,
                        name = currency.sym,
                        description = currency.curDesc,
                        m18Id = currency.id,
                        m18LastModifyDate = LocalDateTime.parse(currency.lastModifyDate, formatter)
                    )

                    currencyService.saveCurrency(currencyRequest)
                    successList += currency.id
                    logger.info("Save Success (M18): ${currency.id}")
                } catch (e: Exception) {
                    failList += currency.id
                    logger.error("Exception")
                    logger.error("Fail Message: ${e.message}")
                    logger.error("Fail Count ${failList.size}: Unit ID - ${currency.id}")
                }
            }
        } else {
            logger.error("Currency List is null. May occur errors.")
        }

        logger.info("Total Save Success (${successList.size})")

        if (failList.size > 0) {
            logger.error("Total Fail (${failList.size}): $failList")
        }

        logger.info("--------------------------------------------End - Saving Currencies--------------------------------------------")

        return SyncResult(
            totalProcessed = successList.size + failList.size,
            totalSuccess = successList.size,
            totalFail = failList.size
        )
    }

    // --------------------------------------------- Bom --------------------------------------------- ///
    open fun getBoms(request: M18CommonRequest): M18BomListResponse? {
        return getList<M18BomListResponse>(
            stSearch = StSearchType.BOM.value,
            params = null,
            conds = beIdConds,
            request = request
        )
    }

    open fun getBom(id: Long): M18BomResponse? {
        logger.info("M18 Bom ID: $id")
        return getLine<M18BomResponse>(
            id = id,
            params = null,
            api = M18_LOAD_BOM_API
        )
    }

    open fun saveBoms(request: M18CommonRequest) {
        logger.info("--------------------------------------------Start - Saving M18 Boms--------------------------------------------")
        val boms = getBoms(request)

        val successList = mutableListOf<Long>()
        val successDetailList = mutableListOf<Long>()
        val failList = mutableListOf<Long>()
        val failDetailList = mutableListOf<Pair<Long, MutableList<Long>>>()
        var failDetailCount = 0

        val values = boms?.values?.sortedBy { it.id }
        if (values != null) {
            values.forEach { bom ->
                try {
                    val bomDetail = getBom(bom.id)
                    val bomUdfBomForShop = bomDetail?.data?.udfbomforshop?.get(0)
                    val bomUdfProduct = bomDetail?.data?.udfproduct

                    logger.info(bomUdfBomForShop.toString())
                    logger.info(bomUdfProduct.toString())
                    if (bomUdfBomForShop != null && bomUdfProduct != null) {
                        // Save Bom
                        val saveBomRequest = SaveBomRequest(
//                            itemId = itemsService.findByNameAndM18UomId(bomUdfBomForShop.desc, bomUdfBomForShop.udfUnit)?.id,
                            code = bomUdfBomForShop.code,
                            name = bomUdfBomForShop.desc,
                            description = bomUdfBomForShop.desc,
                            outputQty = if (bomUdfBomForShop.udfHarvest.trim().toBigDecimalOrNull() != null) bomUdfBomForShop.udfHarvest.trim().toBigDecimal() else BigDecimal(0),
                            outputQtyUom = bomUdfBomForShop.udfHarvestUnit,
                            yield = bomUdfBomForShop.udfYieldratePP,
                            m18UomId = bomUdfBomForShop.udfUnit,
                            m18Id = bomUdfBomForShop.id,
                            m18LastModifyDate = commonUtils.timestampToLocalDateTime(bomUdfBomForShop.lastModifyDate)
                        )
                        val bomId = bomService.saveBom(saveBomRequest).id
                        successList += bom.id

                        // Save Bom Material
                        logger.info("Start saving bom material...")
                        val tempFailList = mutableListOf<Long>()
                        bomUdfProduct.forEach { bomMaterial ->
                            try {
                                val saveBomMaterialRequest = SaveBomMaterialRequest(
                                    m18ItemId = bomMaterial.udfProduct,
                                    itemName = bomMaterial.udfIngredients,
                                    qty = bomMaterial.udfqty,
                                    m18UomId = bomMaterial.udfpurchaseUnit,
                                    uomName = bomMaterial.udfBaseUnit,
                                    bomId = bomId,
                                    m18Id = bomMaterial.id,
                                    m18LastModifyDate = commonUtils.timestampToLocalDateTime(bomUdfBomForShop.lastModifyDate)
                                )
                                bomMaterialService.saveBomMaterial(saveBomMaterialRequest)

                                successDetailList += bomMaterial.id
                            } catch (e: Exception) {
                                tempFailList += bomMaterial.id
                                logger.error("(Bom Material) Exception")
                                logger.error("(Bom Material) Fail Message: ${e.message}")
                                logger.error("(Bom Material) Fail Count ${++failDetailCount}: Bom Material ID - ${bomMaterial.id} | Bom ID - ${bom.id}")
                            }
                        }
                        failDetailList += Pair(bom.id, tempFailList)
                        logger.info("Save Success (M18): ${bom.id}")
                    } else {
                        failList.add(bom.id)
                        logger.error("(Bom) Fail Message: ${bomDetail?.messages?.get(0)?.msgDetail}")
                        logger.error("(Bom) Fail Count ${failList.size}: Bom ID - ${bom.id} Not Found")
                    }
                } catch (e: Exception) {
                    failList += bom.id
                    logger.error("(Bom) Exception")
                    logger.error("(Bom) Fail Message: ${e.message}")
                    logger.error("(Bom) Fail Count ${failList.size}: Bom ID - ${bom.id}")
                }
            }
        } else {
            logger.error("Currency List is null. May occur errors.")
        }

        logger.info("Total Bom Save Success (${successList.size})")
        logger.info("Total Bom Save Detail Success (${successDetailList.size})")

        if (failList.size > 0) {
            logger.error("Total Bom Fail (${failList.size}): $failList")
        }

        if (failDetailCount > 0) {
            logger.error("Total Bom Detail Fail (${failDetailCount}): $failDetailList")
        }

        logger.info("--------------------------------------------End - Saving Boms--------------------------------------------")
    }

    // --------------------------------------------- Business Unit (Shop) --------------------------------------------- ///
    open fun getBusinessUnits(request: M18CommonRequest): M18BusinessUnitListResponse? {
        // seems no beId
        return getList<M18BusinessUnitListResponse>(
            stSearch = StSearchType.BUSINESS_UNIT.value,
            params = null,
//            conds = beIdConds
            request = request
        )
    }

    open fun getBusinessUnit(id: Long): M18BusinessUnitResponse? {
        logger.info("M18 Business Unit ID: $id")
        return getLine<M18BusinessUnitResponse>(
            id = id,
            params = null,
            api = M18_LOAD_BUSINESS_UNIT_API
        )
    }

    open fun saveBusinessUnits(request: M18CommonRequest) : SyncResult {
        logger.info("--------------------------------------------Start - Saving M18 Business Units (Shops)--------------------------------------------")
        val businessUnits = getBusinessUnits(request)

        val successList = mutableListOf<Long>()
        val failList = mutableListOf<Long>()
        val values = businessUnits?.values?.sortedBy { it.id }
        val busMessages = if(businessUnits?.messages?.isNotEmpty() == true) businessUnits.messages[0] else null

        if (values != null) {
            values.forEach { businessUnit ->
//                if (vendor.id in exampleVendors) {
                try {
                    val businessUnitDetail = getBusinessUnit(businessUnit.id)
                    val virdept = businessUnitDetail?.data?.virdept?.get(0)
                    val buMessages = if(businessUnitDetail?.messages?.isNotEmpty() == true) businessUnitDetail?.messages[0] else null

                    if (virdept != null) {

                        val saveShopRequest = SaveShopRequest(
                            id = null,
                            code = virdept.code,
                            name = virdept.descZhTW.ifEmpty { virdept.descZhCN.ifEmpty { virdept.desc } },
                            brNo = null,
                            contactNo = virdept.tel,
                            contactEmail = virdept.email,
                            contactName = null,
                            addr1 = virdept.addr.ifEmpty { virdept.addr_en },
                            addr2 = virdept.addr2.ifEmpty { virdept.addr2_en },
                            addr3 = virdept.addr3.ifEmpty { virdept.addr3_en },
                            addr4 = null,
                            district = null,
                            type = ShopType.SHOP.value,
                            m18Id = businessUnit.id,
                            m18LastModifyDate = commonUtils.timestampToLocalDateTime(virdept.lastModifyDate)
                        )

                        shopService.saveShop(saveShopRequest)
                        successList.add(businessUnit.id)
                        logger.info("Success Count ${successList.size}: ${businessUnit.id} | ${virdept.code} | ${virdept.descZhTW.ifEmpty { virdept.descZhCN.ifEmpty { virdept.desc } }}")
                    } else {
                        failList.add(businessUnit.id)
                        logger.error("(Business Unit) Fail Message: ${buMessages?.msgDetail}")
                        logger.error("(Business Unit) Fail Count ${failList.size}: Business Unit ID - ${businessUnit.id} Not Found")
                    }
                } catch (e: Exception) {
                    failList.add(businessUnit.id)
                    logger.error("(Business Unit) Exception")
                    logger.error("(Business Unit) Fail Message: ${e.message}")
                    logger.error("(Business Unit) Fail Count ${failList.size}: Business Unit ID - ${businessUnit.id}")
                }
//                }
            }
        } else {
            logger.error("(Business Unit) Business Unit List is null. May occur errors.")
            logger.error("(Business Unit)) Fail Message: ${busMessages?.msgDetail}")
            logger.error("(Business Unit) Business Unit List is null. May occur errors.")
        }

        logger.info("Total Success (${successList.size})")

        if (failList.size > 0) {
            logger.error("Total Fail (${failList.size}): $failList")
        }
        logger.info("--------------------------------------------End - Saving M18 Business Units--------------------------------------------")

        return SyncResult(
            totalProcessed = successList.size + failList.size,
            totalSuccess = successList.size,
            totalFail = failList.size
        )
    }
}