package com.ffii.fpsms.modules.common.scheduler.service

import com.ffii.core.utils.JwtTokenUtil
import com.ffii.fpsms.m18.service.M18DeliveryOrderService
import com.ffii.fpsms.m18.service.M18GrnCodeSyncService
import com.ffii.fpsms.m18.service.M18MasterDataService
import com.ffii.fpsms.m18.service.M18PurchaseOrderService
import com.ffii.fpsms.m18.web.models.M18CommonRequest
import com.ffii.fpsms.m18.entity.SchedulerSyncLog
import com.ffii.fpsms.m18.entity.SchedulerSyncLogRepository
import com.ffii.fpsms.m18.model.SyncResult
import com.ffii.fpsms.modules.common.SettingNames
import com.ffii.fpsms.modules.jobOrder.service.JobOrderPlanStartAutoService
import com.ffii.fpsms.modules.master.service.ProductionScheduleService
import com.ffii.fpsms.modules.stock.service.SearchCompletedDnService
import com.ffii.fpsms.modules.stock.service.InventoryLotLineService
import com.ffii.fpsms.modules.settings.entity.Settings
import com.ffii.fpsms.modules.settings.service.SettingsService
import jakarta.annotation.PostConstruct
import org.springframework.beans.factory.annotation.Value
import org.slf4j.Logger
import org.slf4j.LoggerFactory
import org.springframework.scheduling.TaskScheduler
import org.springframework.scheduling.support.CronTrigger
import org.springframework.stereotype.Service
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
import java.util.HashMap
import java.util.concurrent.ScheduledFuture
import kotlin.jvm.optionals.getOrNull

@Service
open class SchedulerService(
    @Value("\${scheduler.postCompletedDnGrn.enabled:true}") val postCompletedDnGrnEnabled: Boolean,
    @Value("\${scheduler.postCompletedDnGrn.receiptDate:}") val postCompletedDnGrnReceiptDate: String,
    @Value("\${scheduler.postCompletedDnGrn.retryMissingGrnLookbackDays:7}") val postCompletedDnGrnRetryMissingGrnLookbackDays: Int,
    @Value("\${scheduler.grnCodeSync.enabled:true}") val grnCodeSyncEnabled: Boolean,
    @Value("\${scheduler.grnCodeSync.syncOffsetDays:0}") val grnCodeSyncSyncOffsetDays: Int,
    @Value("\${scheduler.m18Units.enabled:true}") val m18UnitsSchedulerEnabled: Boolean,
@Value("\${scheduler.m18Units.incrementalLookbackDays:7}") val m18UnitsIncrementalLookbackDays: Int,
@Value("\${scheduler.inventoryLotExpiry.enabled:true}") val inventoryLotExpiryEnabled: Boolean,
    /** When false (default), M18 PO / DO1 / DO2 / master-data cron jobs are not registered — use true in production only. */
    @Value("\${scheduler.m18Sync.enabled:false}") val m18SyncEnabled: Boolean,
    @Value("\${scheduler.jo.planStart.enabled:true}") val jobOrderPlanStartAutoEnabled: Boolean,
    val settingsService: SettingsService,
    /**
     * Lookback window for GRN code sync: rows with `created` from **start of (today − N days)** through **now**,
     * missing `grn_code`. Example: 4 = from 4 days ago 00:00 to now.
     */
    val taskScheduler: TaskScheduler,
    val productionScheduleService: ProductionScheduleService,
    val m18PurchaseOrderService: M18PurchaseOrderService,
    val m18DeliveryOrderService: M18DeliveryOrderService,
    val m18MasterDataService: M18MasterDataService,
    val schedulerSyncLogRepository: SchedulerSyncLogRepository,
    val searchCompletedDnService: SearchCompletedDnService,
    val m18GrnCodeSyncService: M18GrnCodeSyncService,
    val inventoryLotLineService: InventoryLotLineService,
    val jobOrderPlanStartAutoService: JobOrderPlanStartAutoService,
) {
    companion object {
        /** DO2: Spring 6-field cron default and M18 `lastModifyDate` upper bound hour (1pm local). */
        const val DO2_MODIFIED_TO_HOUR: Int = 13
        const val DO2_DEFAULT_CRON: String = "0 0 13 * * *"
        /** Daily 00:00:15 — process job orders whose planStart was yesterday. */
        const val JO_PLAN_START_DEFAULT_CRON: String = "15 0 0 * * *"
    }

    var logger: Logger = LoggerFactory.getLogger(JwtTokenUtil::class.java)
    val dataStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd")
    val dateTimeStringFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
    val defaultCronExpression = "0 0 2 31 2 *";

    //@Volatile
    var scheduledM18Po: ScheduledFuture<*>? = null

    var scheduledM18Do1: ScheduledFuture<*>? = null
    /** Saturday DO1 run (same [getM18Dos1] as [scheduledM18Do1]). */
    var scheduledM18Do1Sat: ScheduledFuture<*>? = null
    var scheduledM18Do2: ScheduledFuture<*>? = null

    @Volatile
    var scheduledM18Master: ScheduledFuture<*>? = null

    var scheduledM18Units: ScheduledFuture<*>? = null

    var scheduledPostCompletedDnGrn: ScheduledFuture<*>? = null

    var scheduledGrnCodeSync: ScheduledFuture<*>? = null
    var scheduledInventoryLotExpiry: ScheduledFuture<*>? = null

    var scheduledJobOrderPlanStart: ScheduledFuture<*>? = null

    //@Volatile
    //var scheduledRoughProd: ScheduledFuture<*>? = null

    //@Volatile
    //var scheduledDetailedProd: ScheduledFuture<*>? = null

    // Common Function
    fun isValidCronExpression(cronExpression: String): Boolean {
        return try {
            CronTrigger(cronExpression)
            true
        } catch (e: IllegalArgumentException) {
            false
        }
    }

    fun commonSchedule(scheduled: ScheduledFuture<*>?, settingName: String, scheduleFunc: () -> Unit): ScheduledFuture<*>? {
        return commonSchedule(scheduled, settingName, defaultCronExpression, scheduleFunc)
    }

    fun commonSchedule(scheduled: ScheduledFuture<*>?, settingName: String, defaultCron: String, scheduleFunc: () -> Unit): ScheduledFuture<*>? {
        scheduled?.cancel(false)

        var cron = settingsService.findByName(settingName).getOrNull()?.value ?: defaultCron

        if (!isValidCronExpression(cron)) {
            cron = defaultCron
        }

        return taskScheduler.schedule(
            { scheduleFunc() },
            CronTrigger(cron)
        )
    }

    /**
     * Same second/minute/hour as [cronExpression] but only Mon–Fri and Sun (Saturday uses [SCHEDULE_M18_DO1_SAT]).
     * Expects a 6-field cron.
     */
    private fun cronToMonFriSunSchedule(cronExpression: String): String {
        val parts = cronExpression.trim().split(Regex("\\s+"))
        if (parts.size != 6) {
            return cronExpression
        }
        return "${parts[0]} ${parts[1]} ${parts[2]} ? * MON-FRI,SUN"
    }

    /**
     * M18 DO1: [SCHEDULE_M18_DO1] applies Mon–Fri & Sun; [SCHEDULE_M18_DO1_SAT] is Saturday only (default 03:10).
     */
    fun scheduleM18Do1() {
        scheduledM18Do1?.cancel(false)
        scheduledM18Do1Sat?.cancel(false)
        if (!m18SyncEnabled) {
            scheduledM18Do1 = null
            scheduledM18Do1Sat = null
            logger.info("M18 DO1 schedulers disabled (scheduler.m18Sync.enabled=false)")
            return
        }

        var cronMain = settingsService.findByName(SettingNames.SCHEDULE_M18_DO1).getOrNull()?.value ?: "0 10 19 * * *"
        if (!isValidCronExpression(cronMain)) {
            cronMain = "0 10 19 * * *"
        }
        val weekdaySunCron = cronToMonFriSunSchedule(cronMain)
        val mainCron = if (isValidCronExpression(weekdaySunCron)) weekdaySunCron else cronMain

        scheduledM18Do1 = taskScheduler.schedule({ getM18Dos1() }, CronTrigger(mainCron))

        var cronSat = settingsService.findByName(SettingNames.SCHEDULE_M18_DO1_SAT).getOrNull()?.value ?: "0 10 3 ? * SAT"
        if (!isValidCronExpression(cronSat)) {
            cronSat = "0 10 3 ? * SAT"
        }
        scheduledM18Do1Sat = taskScheduler.schedule({ getM18Dos1() }, CronTrigger(cronSat))

        logger.info("Scheduled M18 DO1: Mon-Fri,Sun -> {} | Saturday -> {}", mainCron, cronSat)
    }

    // Init Scheduler
    @PostConstruct
    fun init() {
        //scheduleM18PoTask();
        scheduleM18Po();
        scheduleM18Do1();
        scheduleM18Do2();
        scheduleM18MasterData();
        schedulePostCompletedDnGrn();
        scheduleGrnCodeSync();
        scheduleInventoryLotExpiry();
        scheduleJobOrderPlanStartAuto();
        //scheduleRoughProd();
        //scheduleDetailedProd();
    }

    // Scheduler
    // --------------------------- FP-MTMS --------------------------- //
    //fun scheduleRoughProd() {
    //    commonSchedule(scheduledRoughProd, SettingNames.SCHEDULE_PROD_ROUGH, ::getRoughProdSchedule)
    //}

    //fun scheduleDetailedProd() {
    //    commonSchedule(scheduledDetailedProd, SettingNames.SCHEDULE_PROD_DETAILED, ::getDetailedProdSchedule)
    //}

    // --------------------------- M18 --------------------------- //
    fun scheduleM18PoTask() {
        //this is going to delete
        commonSchedule(scheduledM18Po, SettingNames.SCHEDULE_M18_PO, ::getM18Pos)
    }

    fun scheduleM18Po() {
        if (!m18SyncEnabled) {
            scheduledM18Po?.cancel(false)
            scheduledM18Po = null
            logger.info("M18 PO scheduler disabled (scheduler.m18Sync.enabled=false)")
            return
        }
        scheduledM18Po = commonSchedule(scheduledM18Po, SettingNames.SCHEDULE_M18_PO, ::getM18Po)
    }

    fun scheduleM18Do2() {
        if (!m18SyncEnabled) {
            scheduledM18Do2?.cancel(false)
            scheduledM18Do2 = null
            logger.info("M18 DO2 scheduler disabled (scheduler.m18Sync.enabled=false)")
            return
        }
        scheduledM18Do2 = commonSchedule(scheduledM18Do2, SettingNames.SCHEDULE_M18_DO2, DO2_DEFAULT_CRON, ::getM18Dos2)
    }

    fun scheduleM18MasterData() {
        if (!m18SyncEnabled) {
            scheduledM18Master?.cancel(false)
            scheduledM18Master = null
            logger.info("M18 master-data scheduler disabled (scheduler.m18Sync.enabled=false)")
            return
        }
        scheduledM18Master = commonSchedule(scheduledM18Master, SettingNames.SCHEDULE_M18_MASTER, ::getM18MasterData)
    }

    /**
     * Daily sync of M18 units via GET /search/search?stSearch=unit (list + full line per id).
     * Cron from settings [SettingNames.SCHEDULE_M18_UNITS] (default 12:40 local server time).
     * Set scheduler.m18Units.enabled=false to disable.
     */
    fun scheduleM18Units() {
        if (!m18UnitsSchedulerEnabled) {
            scheduledM18Units?.cancel(false)
            scheduledM18Units = null
            logger.info("M18 units scheduler disabled (scheduler.m18Units.enabled=false)")
            return
        }
        commonSchedule(
            scheduledM18Units,
            SettingNames.SCHEDULE_M18_UNITS,
            "0 40 12 * * *",
            ::getM18Units,
        )
    }

    private fun getPostCompletedDnGrnReceiptDate(): LocalDate {
        val s = postCompletedDnGrnReceiptDate.trim()
        return if (s.isEmpty()) LocalDate.now().minusDays(1)
        else try { LocalDate.parse(s) } catch (e: Exception) { LocalDate.now().minusDays(1) }
    }

    /** Backfill grn_code from M18 read/an for logs in [today−syncOffsetDays 00:00, now] missing code. Set scheduler.grnCodeSync.enabled=false to disable. */
    fun scheduleGrnCodeSync() {
        if (!grnCodeSyncEnabled) {
            scheduledGrnCodeSync?.cancel(false)
            scheduledGrnCodeSync = null
            logger.info("GRN code sync scheduler disabled (scheduler.grnCodeSync.enabled=false)")
            return
        }
        commonSchedule(
            scheduledGrnCodeSync,
            SettingNames.SCHEDULE_GRN_CODE_SYNC,
            "0 10 0 * * *",
            { syncGrnCodesFromM18() },
        )
    }

    open fun syncGrnCodesFromM18() {
        try {
            val updated = m18GrnCodeSyncService.syncGrnCodesForLookback(grnCodeSyncSyncOffsetDays)
            logger.info("Scheduler - M18 GRN code sync done (syncOffsetDays=${grnCodeSyncSyncOffsetDays}), rows updated=$updated")
        } catch (e: Exception) {
            logger.error("Scheduler - M18 GRN code sync failed: ${e.message}", e)
        }
    }

    /** Post completed DN and create M18 GRN at 00:01 daily; processes all POs with receipt date = yesterday. Set scheduler.postCompletedDnGrn.enabled=false to disable. */
    fun schedulePostCompletedDnGrn() {
        if (!postCompletedDnGrnEnabled) {
            scheduledPostCompletedDnGrn?.cancel(false)
            scheduledPostCompletedDnGrn = null
            logger.info("PostCompletedDn GRN scheduler disabled (scheduler.postCompletedDnGrn.enabled=false)")
            return
        }
        commonSchedule(
            scheduledPostCompletedDnGrn,
            SettingNames.SCHEDULE_POST_COMPLETED_DN_GRN,
            "0 1 0 * * *",
            { getPostCompletedDnAndProcessGrn(receiptDate = getPostCompletedDnGrnReceiptDate()) }
        )
    }

    /**
     * Job order plan-start batch at 00:00:15 daily (yesterday plan day).
     * Set scheduler.jo.planStart.enabled=false to disable.
     */
    fun scheduleJobOrderPlanStartAuto() {
        if (!jobOrderPlanStartAutoEnabled) {
            scheduledJobOrderPlanStart?.cancel(false)
            scheduledJobOrderPlanStart = null
            logger.info("Job order plan-start auto scheduler disabled (scheduler.jo.planStart.enabled=false)")
            return
        }
        scheduledJobOrderPlanStart = commonSchedule(
            scheduledJobOrderPlanStart,
            SettingNames.SCHEDULE_JO_PLAN_START,
            JO_PLAN_START_DEFAULT_CRON,
            ::runJobOrderPlanStartAuto,
        )
        logger.info("Scheduled job order plan-start auto (default cron={})", JO_PLAN_START_DEFAULT_CRON)
    }

    open fun runJobOrderPlanStartAuto() {
        try {
            val report = jobOrderPlanStartAutoService.runAutoProcess(LocalDateTime.now())
            logger.info(
                "Scheduler - Job order plan-start auto: candidates={}, hidden={}, rescheduled={}, skipped={}, errors={}",
                report.candidates,
                report.hidden,
                report.rescheduled,
                report.skipped,
                report.errors,
            )
        } catch (e: Exception) {
            logger.error("Scheduler - Job order plan-start auto failed: ${e.message}", e)
        }
    }

    /** Mark expired inventory lot lines as unavailable daily. Set scheduler.inventoryLotExpiry.enabled=false to disable. */
    fun scheduleInventoryLotExpiry() {
        if (!inventoryLotExpiryEnabled) {
            scheduledInventoryLotExpiry?.cancel(false)
            scheduledInventoryLotExpiry = null
            logger.info("Inventory lot expiry scheduler disabled (scheduler.inventoryLotExpiry.enabled=false)")
            return
        }
        scheduledInventoryLotExpiry = commonSchedule(
            scheduledInventoryLotExpiry,
            SettingNames.SCHEDULE_INVENTORY_LOT_EXPIRY,
            "0 5 0 * * *",
            { markExpiredInventoryLotLinesAsUnavailable() }
        )
    }

    open fun markExpiredInventoryLotLinesAsUnavailable() {
        try {
            val updatedRows = inventoryLotLineService.markExpiredLotLinesAsUnavailable()
            logger.info("Scheduler - Inventory lot expiry update done, rows updated=$updatedRows")
        } catch (e: Exception) {
            logger.error("Scheduler - Inventory lot expiry update failed: ${e.message}", e)
        }
    }

    // Function for schedule
    // --------------------------- FP-MTMS --------------------------- //
    
    open fun getRoughProdSchedule() {
        try {
            logger.info("Daily Scheduler - Rough Prod")
            val demoFGList = productionScheduleService.convertToFinishedGoodList();

            val result: HashMap<ProductionScheduleService.RoughScheduleObj, Double> = productionScheduleService.generateRoughScheduleByWeek(demoFGList)
            val sortedEntries = result.entries.sortedBy { it.value }

            var accProdCount = 0.0;
            var fgCount = 0L;
            for ((roughScheduleRecord, totalDifference) in sortedEntries) {
                accProdCount += roughScheduleRecord.totalProductionCount;
                fgCount ++;
                println("[totalDifference:" + totalDifference + "] - " + roughScheduleRecord.toString())
            }

            // Convert to List<HashMap<RoughScheduleObj, Double>>
            productionScheduleService.saveRoughScheduleOutput(sortedEntries, accProdCount, fgCount);
        } catch (e: Exception) {
            throw RuntimeException("Error generate schedule: ${e.message}", e)
        }
    }
    
    open fun getDetailedProdSchedule() {
        try {
            logger.info("Daily Scheduler - Detailed Prod")
            // For test
            val today = LocalDateTime.now()

            // T, T+1, T+2
            for (day in longArrayOf(0L, 1L, 2L)) {
                val produceAt = today.plusDays(day)
                val latestRoughScheduleAt = productionScheduleService.getScheduledAtByDate(produceAt.toLocalDate());
                // assume schedule period is monday to sunday
                val assignDate = produceAt.dayOfWeek.value

                //TODO: update this to receive selectedDate and assignDate from schedule
                println("produceAt: $produceAt | today： $today | latestRoughScheduleAt: $latestRoughScheduleAt | assignDate: $assignDate ")
                productionScheduleService.generateDetailedScheduleByDay(assignDate, latestRoughScheduleAt.atStartOfDay(), produceAt, 7)
            }
        } catch (e: Exception) {
            throw RuntimeException("Error generate schedule: ${e.message}", e)
        }
    }
    // --------------------------- M18 --------------------------- //
//    @Async
//    @Scheduled(cron = "0 0 2 * * *") // (SS/MM/HH/DD/MM/YY)
    open fun getM18Pos() {
        logger.info("Daily Scheduler - PO")
        val currentTime = LocalDateTime.now()
        val today = currentTime.toLocalDate().atStartOfDay()
        /*        val yesterday = today.minusDays(1L)
        val request = M18CommonRequest(
            modifiedDateTo = today.format(dataStringFormat),
            modifiedDateFrom = yesterday.format(dataStringFormat)
        )*/
        val tmr = today.plusDays(1L)
        var request = M18CommonRequest(
            dDateTo = tmr.format(dataStringFormat),
            dDateFrom = tmr.format(dataStringFormat)
        )
        m18PurchaseOrderService.savePurchaseOrders(request);

        //dDate from tmr to tmr
        var requestDO = M18CommonRequest(
            dDateTo = tmr.format(dataStringFormat),
            dDateFrom = tmr.format(dataStringFormat)
        )
        m18DeliveryOrderService.saveDeliveryOrders(requestDO);
//        logger.info("today: ${today.format(dataStringFormat)}")
//        logger.info("yesterday: ${yesterday.format(dataStringFormat)}")
    }

    open fun getM18Po() {
        logger.info("PO Scheduler")
        val currentTime = LocalDateTime.now()
        val today = currentTime.toLocalDate().atStartOfDay()
        val TenDaysLater = today.plusDays(10L)

        var requestDO = M18CommonRequest(
            dDateTo = TenDaysLater.format(dateTimeStringFormat),
            dDateFrom = today.format(dateTimeStringFormat)
        )
     
        val result = m18PurchaseOrderService.savePurchaseOrders(requestDO);

        saveSyncLog(
            type = "PO", 
            status = "SUCCESS", 
            result = result, 
            start = currentTime
        )
    }
    
    open fun getM18Dos1() {
        logger.info("DO Scheduler 1 - DO")
        val currentTime = LocalDateTime.now()
        val today = currentTime.toLocalDate().atStartOfDay()
        val twoDaysLater = today.plusDays(2L)

        var requestDO = M18CommonRequest(
            dDateTo = twoDaysLater.format(dateTimeStringFormat),
            dDateFrom = twoDaysLater.format(dateTimeStringFormat)
        )
     
        val result = m18DeliveryOrderService.saveDeliveryOrders(requestDO);

        saveSyncLog(
            type = "DO1", 
            status = "SUCCESS", 
            result = result, 
            start = currentTime
        )
        
    }

    private fun saveSyncLog(type: String, status: String, result: SyncResult? = null, error: String? = null, start: LocalDateTime) {
        val log = SchedulerSyncLog(
            syncType = type,
            status = status,
            recordsProcessed = result?.totalProcessed ?: 0,
            recordsSaved = result?.totalSuccess ?: 0,
            recordsFailed = result?.totalFail ?: 0,
            errorMessage = error,
            query = result?.query ?: "",
            startTime = start,
            endTime = LocalDateTime.now()
        )
        schedulerSyncLogRepository.save(log)
    }

    open fun getM18Dos2() {
        logger.info("DO Scheduler 2 - DO")
        val currentTime = LocalDateTime.now()

        // .atStartOfDay() results in 00:00:00
        val runDate = currentTime.toLocalDate()
        val today = runDate.atStartOfDay()
        val ysd = today.minusDays(1L)
        val tmr = today.plusDays(1L)

        // Default: lastModified from yesterday 19:00 through today's DO2 run hour (1pm; aligns with SCHEDULE.m18.do2).
        // On Sunday, yesterday is Saturday: use 03:00 instead so we include DO changed after Sat 03:10 DO1
        // (otherwise Sat 03:00–18:59 would be skipped until a much later sync).
        val isSundayDo2 = runDate.dayOfWeek == DayOfWeek.SUNDAY
        val modifiedFromStart = if (isSundayDo2) {
            ysd.withHour(3).withMinute(0).withSecond(0)
        } else {
            ysd.withHour(19).withMinute(0).withSecond(0)
        }

        val modifiedDateToEnd =
            today.withHour(DO2_MODIFIED_TO_HOUR).withMinute(0).withSecond(0)

        logger.info(
            "DO2 modifiedDateFrom={} ({}), modifiedDateTo={}",
            modifiedFromStart.format(dateTimeStringFormat),
            if (isSundayDo2) "Sunday window from Sat 03:00" else "from yesterday 19:00",
            modifiedDateToEnd.format(dateTimeStringFormat),
        )

        val requestDO = M18CommonRequest(
            // These will now produce "yyyy-MM-dd HH:mm:ss"
            dDateTo = tmr.format(dateTimeStringFormat),           // e.g. 2026-01-19 00:00:00
            dDateFrom = tmr.format(dateTimeStringFormat),         // e.g. 2026-01-19 00:00:00
            modifiedDateTo = modifiedDateToEnd.format(dateTimeStringFormat),
            modifiedDateFrom = modifiedFromStart.format(dateTimeStringFormat),
        )
    
        val result = m18DeliveryOrderService.saveDeliveryOrders(requestDO)

        saveSyncLog(
            type = "DO2", 
            status = "SUCCESS", 
            result = result, 
            start = currentTime
        )
    }

    open fun getPostCompletedDnAndProcessGrn(
        receiptDate: java.time.LocalDate? = null,
        skipFirst: Int = 0,
        limitToFirst: Int? = null,
    ) {
        logger.info("Scheduler - Post completed DN and process GRN")
        val date = receiptDate ?: java.time.LocalDate.now().minusDays(1)
        searchCompletedDnService.postCompletedDnAndProcessGrnWithMissingRetry(
            receiptDate = date,
            skipFirst = skipFirst,
            limitToFirst = limitToFirst,
            retryLookbackDays = postCompletedDnGrnRetryMissingGrnLookbackDays,
        )
    }

    open fun getM18MasterData() {
        logger.info("Daily Scheduler - Master Data")
        m18MasterDataService.ensureCunitSeededForAllIfEmpty()
        var currentTime = LocalDateTime.now()
        val today = currentTime.toLocalDate().atStartOfDay()
        val yesterday = today.minusDays(1L)
        val request = M18CommonRequest(
            modifiedDateTo = today.format(dataStringFormat),
            modifiedDateFrom = yesterday.format(dataStringFormat)
        )

        // Keep units as part of master-data sync and run it first with the same modified-date window.
        val resultUnits = m18MasterDataService.saveUnits(request)
        saveSyncLog(
            type = "Units",
            status = "SUCCESS",
            result = resultUnits,
            start = currentTime
        )

        currentTime = LocalDateTime.now()
        val resultProducts = m18MasterDataService.saveProducts(request)
        saveSyncLog(
            type = "Products", 
            status = "SUCCESS", 
            result = resultProducts, 
            start = currentTime
        )
        currentTime = LocalDateTime.now()
        val resultProblemItemUomResync = m18MasterDataService.resyncProblemItemUomsFromM18()
        saveSyncLog(
            type = "ProductsUomResync",
            status = "SUCCESS",
            result = resultProblemItemUomResync,
            start = currentTime
        )
//        m18MasterDataService.saveBoms(request)
        currentTime = LocalDateTime.now()
        val resultVendors = m18MasterDataService.saveVendors(request)
        saveSyncLog(
            type = "Vendors", 
            status = "SUCCESS", 
            result = resultVendors, 
            start = currentTime
        )

        val resultBusinessUnits = m18MasterDataService.saveBusinessUnits(request)
        saveSyncLog(
            type = "BusinessUnits", 
            status = "SUCCESS", 
            result = resultBusinessUnits, 
            start = currentTime
        )

        val resultCurrencies = m18MasterDataService.saveCurrencies(request)
        saveSyncLog(
            type = "Currencies", 
            status = "SUCCESS", 
            result = resultCurrencies, 
            start = currentTime
        )
//        logger.info("today: ${today.format(dataStringFormat)}")
//        logger.info("yesterday: ${yesterday.format(dataStringFormat)}")

        
    }

    /**
     * M18 units: first run (until setting [SettingNames.M18_UNITS_SYNC_INITIAL_FULL_SYNC_DONE] is true)
     * calls search with **no** lastModifyDate filter (full list). After a successful full run, that flag is set
     * and subsequent runs use lastModifyDate from **(today − [m18UnitsIncrementalLookbackDays])** through **today**.
     */
    open fun getM18Units() {
        logger.info("Scheduler - M18 units (search/search?stSearch=unit)")
        val currentTime = LocalDateTime.now()
        val today = currentTime.toLocalDate().atStartOfDay()
        val todayStr = today.format(dataStringFormat)

        val initialFullSyncDone = settingsService.findByName(SettingNames.M18_UNITS_SYNC_INITIAL_FULL_SYNC_DONE)
            .map { Settings.VALUE_BOOLEAN_TRUE == it.value }
            .orElse(false)

        val request = if (initialFullSyncDone) {
            val fromStr = today.minusDays(m18UnitsIncrementalLookbackDays.toLong()).format(dataStringFormat)
            logger.info(
                "M18 units sync mode=INCREMENTAL lastModifyDate from=$fromStr to=$todayStr " +
                    "(lookbackDays=$m18UnitsIncrementalLookbackDays)",
            )
            M18CommonRequest(
                modifiedDateFrom = fromStr,
                modifiedDateTo = todayStr,
            )
        } else {
            logger.info("M18 units sync mode=FULL (no lastModifyDate filter; first successful run will switch to incremental)")
            M18CommonRequest(
                modifiedDateFrom = null,
                modifiedDateTo = null,
            )
        }

        val result = m18MasterDataService.saveUnits(request)
        saveSyncLog(
            type = "Units",
            status = "SUCCESS",
            result = result,
            start = currentTime,
        )

        if (!initialFullSyncDone && result.totalFail == 0) {
            settingsService.update(SettingNames.M18_UNITS_SYNC_INITIAL_FULL_SYNC_DONE, Settings.VALUE_BOOLEAN_TRUE)
            logger.info(
                "M18 units: initial full sync finished with no failures; " +
                    "setting ${SettingNames.M18_UNITS_SYNC_INITIAL_FULL_SYNC_DONE}=true — next runs use incremental window.",
            )
        }
    }
}