package com.ffii.fpsms.modules.qc.service

import com.ffii.core.support.JdbcDao
import com.ffii.fpsms.modules.jobOrder.entity.JobOrderRepository
import com.ffii.fpsms.modules.purchaseOrder.entity.PurchaseOrderRepository
import com.ffii.fpsms.modules.qc.entity.QcFlowType
import com.ffii.fpsms.modules.qc.entity.QcMeasureType
import com.ffii.fpsms.modules.qc.entity.QcResult
import com.ffii.fpsms.modules.qc.entity.QcResultMeasurement
import com.ffii.fpsms.modules.qc.entity.QcResultMeasurementRepository
import com.ffii.fpsms.modules.qc.web.model.SaveQcResultRequest
import com.ffii.fpsms.modules.stock.entity.StockInLine
import org.slf4j.LoggerFactory
import org.springframework.dao.DataAccessException
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Service
open class QcResultMeasurementService(
    private val qcResultMeasurementRepository: QcResultMeasurementRepository,
    private val purchaseOrderRepository: PurchaseOrderRepository,
    private val jobOrderRepository: JobOrderRepository,
    private val jdbcDao: JdbcDao,
) {
    private val logger = LoggerFactory.getLogger(QcResultMeasurementService::class.java)

    @Volatile
    private var measurementTableReady: Boolean? = null

    @Volatile
    private var loggedMissingTableWarning: Boolean = false

    @Transactional
    open fun syncMeasurement(
        request: SaveQcResultRequest,
        savedQcResult: QcResult,
        stockInLine: StockInLine,
    ) {
        if (request.measurement == null) {
            return
        }
        if (!isMeasurementStorageReady()) {
            return
        }

        try {
            doSyncMeasurement(request, savedQcResult, stockInLine)
        } catch (ex: DataAccessException) {
            if (isMissingMeasurementTable(ex)) {
                measurementTableReady = false
                logMissingTableOnce()
                return
            }
            throw ex
        }
    }

    private fun doSyncMeasurement(
        request: SaveQcResultRequest,
        savedQcResult: QcResult,
        stockInLine: StockInLine,
    ) {
        val measurementRequest = request.measurement!!
        val measureType = measurementRequest.measureType?.takeIf { it.isNotBlank() }
            ?: QcMeasureType.TEMPERATURE
        val existing = savedQcResult.id?.let {
            qcResultMeasurementRepository.findByQcResultIdAndMeasureTypeAndDeletedFalse(it, measureType)
        }

        val value = measurementRequest.value
        if (value == null) {
            existing?.let {
                it.deleted = true
                qcResultMeasurementRepository.saveAndFlush(it)
            }
            return
        }

        val measurement = existing ?: QcResultMeasurement()
        measurement.apply {
            qcResult = savedQcResult
            this.stockInLine = stockInLine
            qcType = resolveQcType(stockInLine)
            orderRefCode = resolveOrderRefCode(stockInLine)
            this.measureType = measureType
            this.value = value
            unit = measurementRequest.unit?.takeIf { it.isNotBlank() } ?: defaultUnitFor(measureType)
            deleted = false
        }
        qcResultMeasurementRepository.saveAndFlush(measurement)
    }

    /**
     * Returns false when Liquibase migration for [qc_result_measurement] has not been applied yet,
     * so stock-in QC can complete without rolling back the parent transaction.
     */
    private fun isMeasurementStorageReady(): Boolean {
        if (measurementTableReady == true) {
            return true
        }

        return try {
            val exists = jdbcDao.queryForInt(
                """
                SELECT COUNT(*)
                FROM information_schema.tables
                WHERE table_schema = DATABASE()
                  AND table_name = 'qc_result_measurement'
                """.trimIndent(),
            ) > 0
            measurementTableReady = exists
            if (!exists) {
                logMissingTableOnce()
            }
            exists
        } catch (ex: Exception) {
            measurementTableReady = false
            if (!loggedMissingTableWarning) {
                logger.warn(
                    "Unable to verify qc_result_measurement table; skipping QC measurement sync until migration is applied.",
                    ex,
                )
                loggedMissingTableWarning = true
            }
            false
        }
    }

    private fun logMissingTableOnce() {
        if (loggedMissingTableWarning) {
            return
        }
        logger.warn(
            "qc_result_measurement table is not available; skipping QC measurement sync. " +
                "Apply Liquibase migration 20260622_01_qc_result_measurement to persist temperature/humidity values.",
        )
        loggedMissingTableWarning = true
    }

    private fun isMissingMeasurementTable(ex: Throwable): Boolean {
        var current: Throwable? = ex
        while (current != null) {
            val message = current.message?.lowercase().orEmpty()
            if (
                message.contains("qc_result_measurement") &&
                (message.contains("doesn't exist") ||
                    message.contains("does not exist") ||
                    message.contains("unknown table") ||
                    message.contains("invalid object name"))
            ) {
                return true
            }
            current = current.cause
        }
        return false
    }

    open fun resolveQcType(stockInLine: StockInLine): String {
        return if (stockInLine.jobOrder?.id != null) {
            QcFlowType.EPQC
        } else {
            QcFlowType.IQC
        }
    }

    open fun resolveOrderRefCode(stockInLine: StockInLine): String? {
        val jobOrderId = stockInLine.jobOrder?.id
        if (jobOrderId != null) {
            return stockInLine.jobOrder?.code
                ?: jobOrderRepository.findById(jobOrderId).orElse(null)?.code
        }
        val purchaseOrderId = stockInLine.purchaseOrder?.id
        if (purchaseOrderId != null) {
            return stockInLine.purchaseOrder?.code
                ?: purchaseOrderRepository.findById(purchaseOrderId).orElse(null)?.code
        }
        return null
    }

    private fun defaultUnitFor(measureType: String): String? {
        return when (measureType) {
            QcMeasureType.TEMPERATURE -> "°C"
            QcMeasureType.HUMIDITY -> "%"
            else -> null
        }
    }
}
