package com.doverfuelingsolutions.issp.fusion

import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent
import com.doverfuelingsolutions.issp.R
import com.doverfuelingsolutions.issp.api.FuelInfoApi
import com.doverfuelingsolutions.issp.api.dto.DFSResult
import com.doverfuelingsolutions.issp.api.entity.PumpInfo
import com.doverfuelingsolutions.issp.data.GlobalData
import com.doverfuelingsolutions.issp.fusion.callback.OnFusionStatus
import com.doverfuelingsolutions.issp.utils.StringUtil
import com.doverfuelingsolutions.issp.utils.ValidateUtil
import com.doverfuelingsolutions.issp.utils.log.DFSLog
import com.doverfuelingsolutions.issp.utils.sp.SPKeys
import com.doverfuelingsolutions.issp.utils.sp.SPUtil
import com.doverfuelingsolutions.issp.utils.thread.ThreadUtil
import com.wayne.www.waynelib.fdc.FdcClient
import com.wayne.www.waynelib.fdc.OnFdcClientStateChangedListener
import com.wayne.www.waynelib.fdc.OnFdcMessageReceivedListener
import com.wayne.www.waynelib.fdc.OnFdcServiceResponseReceivedListener
import com.wayne.www.waynelib.fdc.message.*
import kotlinx.coroutines.*
import java.util.*
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.math.min

object FusionManager : LifecycleObserver, OnFdcClientStateChangedListener,
    OnFdcServiceResponseReceivedListener,
    OnFdcMessageReceivedListener {

    private var isInitialized = false
    var stateFusion: FdcClient.FdcClientState = FdcClient.FdcClientState.Stopped
    private const val timeoutMax = 4000
    private var reconnectTime = 0
    private var isKeepDisconnected = false

    var isLogin = false
        private set
    var isFetchPumpInfo = false
        private set
    val pumpList = arrayListOf<PumpInfo>()

    private val coroutineIO = CoroutineScope(Dispatchers.IO)
    private var jobRestart: Job? = null

    var onFusionStatus: OnFusionStatus? = null

    override fun onFdcClientStateChanged(sender: FdcClient?, state: FdcClient.FdcClientState?) {
        DFSLog.d("Fusion state changed: ${state?.name?.toLowerCase(Locale.CHINESE)}")
        if (sender == null || state == null) return

        stateFusion = state
        // Reset reconnectTime.
        if (state != FdcClient.FdcClientState.Connecting && state != FdcClient.FdcClientState.MyAddReConnect) {
            isKeepDisconnected = false
            if (reconnectTime > 0) {
                reconnectTime = 0
            }
        }
        when (state) {
            FdcClient.FdcClientState.Connected -> {
                onFusionStatus?.onFusionStatus(FusionStatus.Connected)
                coroutineIO.launch {
                    val deferred1 = async { loginToFusion() }
                    val deferred2 = async { getPumpInfo() }
                    deferred1.await()
                    deferred2.await()
                }
            }
            FdcClient.FdcClientState.Connecting -> {
                onFusionStatus?.onFusionStatus(FusionStatus.Connecting)
            }
            FdcClient.FdcClientState.DisconnectedByHeartbeat -> {
                onFusionStatus?.onFusionStatus(FusionStatus.Connecting)
                coroutineIO.launch {
                    // Try restart if current client remains Connecting or DisconnectedByHeartbeat after delay.
                    delay(2000)
                    if (stateFusion == FdcClient.FdcClientState.Connecting || stateFusion == FdcClient.FdcClientState.DisconnectedByHeartbeat) {
                        // Result of restart has to be MyAddReConnect or Connected so the custom Connecting status triggered above can't last forever.
                        DFSLog.i("Try restart fusion client as current client remains Connecting or DisconnectedByHeartbeat after delay")
                        restart()
                    }
                }
            }
            FdcClient.FdcClientState.Stopped -> {
                // Try restart if client is not stopped manually.
                if (isInitialized) {
                    onFusionStatus?.onFusionStatus(FusionStatus.Connecting)
                    // Result of restart has to be MyAddReConnect or Connected.
                    restart()
                }
            }
            FdcClient.FdcClientState.MyAddReConnect -> {
                // Status MyAddReConnect means client socket has been closed.
                // Restart might or might not works.
                // Prevent infinite loop cause restart could also trigger MyAddReConnect.
                jobRestart = coroutineIO.launch {
                    isKeepDisconnected = true

                    val delayTime = reconnectTime * 10L
                    reconnectTime = min(reconnectTime + 1, 6)
                    if (delayTime > 0) {
                        onFusionStatus?.onFusionStatus(FusionStatus.Disconnected)
                        delay(delayTime * 1000)
                    }
                    if (isKeepDisconnected) {
                        DFSLog.d("Try to restart 'Disconnected' fusion client.")
                        onFusionStatus?.onFusionStatus(FusionStatus.Connecting)
                        restart()
                    }
                }
            }
        }
    }

    override fun onServiceResponseReceived(sender: FdcClient?, serviceResponse: ServiceResponse?) {}

    override fun onFdcMessageReceived(sender: FdcClient?, fdcMessage: FdcMessage?) {}

    @OnLifecycleEvent(Lifecycle.Event.ON_CREATE)
    fun initialize() {
        DFSLog.i("FusionManager is initializing.")
        isInitialized = true
        FdcClient.getDefault().addOnFdcClientStateChangedListeners(this)
        FdcClient.getDefault().addOnFdcServiceResponseReceivedListeners(this)
        FdcClient.getDefault().addOnFdcMessageReceivedListeners(this)
        FdcClient.getDefault().start(
            SPUtil.getString(SPKeys.MIDDLE_IP),
            SPUtil.getString(SPKeys.MIDDLE_PORT).toInt(),
            GlobalData.serialNumber.get(),
            SPUtil.getString(SPKeys.MIDDLE_WORKSTATION_ID).toInt(),
            null
        )
    }

    @OnLifecycleEvent(Lifecycle.Event.ON_DESTROY)
    fun close() {
        DFSLog.i("FusionManager is closing.")
        isInitialized = false
        isLogin = false
        isFetchPumpInfo = false

        val manager = this
        coroutineIO.launch {
            logout()
            FdcClient.getDefault().removeOnFdcClientStateChangedListeners(manager)
            FdcClient.getDefault().removeOnFdcServiceResponseReceivedListeners(manager)
            FdcClient.getDefault().removeOnFdcMessageReceivedListeners(manager)
            FdcClient.getDefault().stop()
        }
    }

    /**
     * Restart fusion client.
     *
     * @param immediate FusionManager has an internal restart loop when client remains disconnected. This restart loop will be broken as 'immediate' is true.
     */
    fun restart(immediate: Boolean = false) {
        DFSLog.d("Fusion client is restarting.")
        if (immediate) {
            isKeepDisconnected = false
            reconnectTime = 0
            if (jobRestart?.isActive == true) jobRestart?.cancel()
        }
        coroutineIO.launch {
            close()
            delay(timeoutMax.toLong() + 1000L)
            initialize()
        }
    }

    /**
     * Login to Fusion and retry if first login failed.
     */
    suspend fun loginToFusion(): Boolean = suspendCoroutine {
        coroutineIO.launch {
            isLogin = if (login()) true else {
                DFSLog.d("Fusion: retry login after logout")
                logout()
                login()
            }
            it.resume(isLogin)
        }
    }

    /**
     * Fetch all uncleared order.
     * @param pumpId -1 means all pumps, others mean specified pump.
     */
    suspend fun getAllUnsettledOrder(pumpId: Int = -1) = suspendCoroutine<DFSResult<List<DeviceClass>>> {
            ThreadUtil.io {
                val time1 = System.currentTimeMillis()
                FdcClient.getDefault().sendGetAvailableFuelSaleTrxs(pumpId, { _, response ->
                    val time2 = System.currentTimeMillis()
                    DFSLog.v(
                        "fetch uncleared orders of pump $pumpId (cost ${time2 - time1}ms)",
                        response
                    )
                    coroutineIO.launch {
                        if (response == null || response !is ServiceResponseGetAvailableFuelSaleTrxs) {
                            it.resume(DFSResult.fail(R.string.fail_get_order))
                        } else if (response.singleFdcData == null || response.singleDeviceClass == null) {
                            it.resume(DFSResult.success(emptyList()))
                        } else {
                            val unsettledList =
                                response.singleFdcData.deviceClasses.filter { !it.isClear }
                            val resultDetailList = unsettledList
                                .map { dc -> async { getOrderDetail(dc) } }
                                .map { deferred -> deferred.await() }
                                .filter { it.success }
                                .mapNotNull {
                                    it.data?.productName =
                                        pumpList.find { pump -> pump.pumpId == it.data?.pumpNo }?.oilName
                                            ?: ""
                                    it.data
                                }
                            val time3 = System.currentTimeMillis()
                            DFSLog.v("fetch uncleared orders detail cost ${time3 - time2}ms")
                            it.resume(DFSResult.success(resultDetailList))
                        }
                    }
                }, 8000)
            }
        }

    /**
     * Fetch order detail.
     */
    private suspend fun getOrderDetail(deviceClass: DeviceClass) = suspendCoroutine<DFSResult<DeviceClass>> {
            getOrderDetailAsync(deviceClass) { asyncResult ->
                it.resume(asyncResult)
            }
        }

    /**
     * Fetch order detail.
     */
    private fun getOrderDetailAsync(dcRaw: DeviceClass, callback: (result: DFSResult<DeviceClass>) -> Unit) {
        ThreadUtil.io {
            FdcClient.getDefault().sendGetFuelSalesTrxDetailsRequestAsync(
                dcRaw.pumpNo,
                dcRaw.transactionSeqNo,
                dcRaw.releaseTokenAttribute,
                { _, response ->
                    DFSLog.v(
                        "fetch order detail of ${dcRaw.pumpNo}-${dcRaw.transactionSeqNo}",
                        response
                    )
                    if (response == null || response !is ServiceResponseGetFuelSalesTrxDetails || response.fdcData.isEmpty() || response.fdcData[0].deviceClasses.isEmpty()) {
                        callback(DFSResult.fail(R.string.fail_get_order_detail))
                    } else {
                        val dc = response.fdcData[0].deviceClasses[0]
                        // 迷惑的数据需要 hack 一下
                        dc.releaseTokenAttribute = dcRaw.releaseTokenAttribute
                        val correctNozzle = pumpList.find {
                            it.pumpId == dc.pumpNo
                        }?.nozzleList?.find {
                            it.logicId == dc.nozzleNo
                        }
                        correctNozzle?.let {
                            dc.nozzleNo = it.physicalId
                            dc.logicNo = it.logicId
                        }
                        callback(DFSResult.success(dc))
                    }
                },
                3000
            )
        }
    }

    /**
     * Clear order in server. Also remember mark order status as 'Cleared'.
     */
    suspend fun clearOrder(dc: DeviceClass): DFSResult<Boolean> {
        return if (dc.isClear) {
            DFSResult.success(true)
        } else if (dc.releaseTokenAttribute == null || !ValidateUtil.isPositiveInt(dc.releaseTokenAttribute)) {
            DFSResult.fail(R.string.wrong_order_info)
        } else {
            clearOrder(dc.pumpNo, dc.transactionSeqNo, dc.releaseTokenAttribute.toInt())
        }
    }

    suspend fun clearOrder(pumpId: Int, transactionNo: String, token: Int) = suspendCoroutine<DFSResult<Boolean>> {
            ThreadUtil.io {
                FdcClient.getDefault()
                    .sendClearFuelSaleTrxRequestAsync(pumpId, transactionNo, token, { _, response ->
                        if (response != null && response.singleDeviceClass != null && response.overallResult.equals(
                                "Success",
                                true
                            )
                        ) {
                            DFSLog.i("clear order[$pumpId-$transactionNo-$token] succeed")
                            it.resume(DFSResult.success(true))
                        } else {
                            DFSLog.e("FusionManager.clearOrder failed", response)
                            it.resume(DFSResult.fail(R.string.fail_lock_order))
                        }
                    }, 4000)
            }
        }

    /**
     * Lock or unlock order.
     */
    suspend fun lockOrder(dc: DeviceClass, lock: Boolean): DFSResult<Boolean> {
        return if (dc.isLock == lock) {
            DFSResult.success(true)
        } else if (dc.releaseTokenAttribute == null || !ValidateUtil.isPositiveInt(dc.releaseTokenAttribute)) {
            DFSResult.fail(R.string.wrong_order_info)
        } else {
            lockOrder(dc.pumpNo, dc.transactionSeqNo, dc.releaseTokenAttribute.toInt(), lock)
        }
    }

    suspend fun lockOrder(pumpId: Int, transactionNo: String, token: Int, lock: Boolean) = suspendCoroutine<DFSResult<Boolean>> {
            ThreadUtil.io {
                val callback = OnFdcServiceResponseReceivedListener { _, response ->
                    DFSLog.v("Fusion.lockOrder($pumpId, $transactionNo, $token, $lock)", response)
                    if (response != null && response.singleDeviceClass != null && response.overallResult.equals("Success", true)) {
                        it.resume(DFSResult.success(true))
                    } else {
                        it.resume(DFSResult.fail(response?.overallResult ?: StringUtil.get(R.string.fail_operate)))
                    }
                }
                if (lock) {
                    FdcClient.getDefault().sendLockFuelSaleTrxRequestAsync(
                        pumpId,
                        transactionNo,
                        token,
                        callback,
                        4000
                    )
                } else {
                    FdcClient.getDefault().sendUnLockFuelSaleTrxRequestAsync(
                        pumpId,
                        transactionNo,
                        token,
                        callback,
                        4000
                    )
                }
            }
        }

    /**
     * Proxy method to get pump and nozzle data.
     */
    suspend fun getPumpInfo(): DFSResult<List<PumpInfo>> {
        val resultPumpList = FuelInfoApi.requestPumpInfo()
        if (resultPumpList.success && resultPumpList.data != null) {
            isFetchPumpInfo = true
            if (pumpList.isNotEmpty()) pumpList.clear()
            pumpList.addAll(resultPumpList.data)
        }

        return resultPumpList
    }

    private suspend fun login() = suspendCoroutine<Boolean> {
        val port = SPUtil.getString(SPKeys.MIDDLE_PORT).toInt()
        FdcClient.getDefault().sendLogonRequestAsync(port, port, "00.07", { _, response ->
            DFSLog.v("Fusion: login response", response)
            if (response != null && response is ServiceResponseLogOn) {
                val status = response.singleFdcData.fdcStatus
                if (status.equals(FusionConstants.CODE_SUCCESS, true)) {
                    DFSLog.d("Fusion: login succeeded")
                    it.resume(true)
                } else {
                    DFSLog.e("Fusion: login failed")
                    it.resume(false)
                }
            }
        }, timeoutMax)
    }

    private suspend fun logout() = suspendCoroutine<Boolean> {
        isLogin = false
        FdcClient.getDefault().sendLogOffRequestAsync({ _, response ->
            val status = response?.singleFdcData?.fdcStatus
            DFSLog.v("Fusion: try logout response = $status")
            if (status != null && status.equals(FusionConstants.CODE_SUCCESS, true)) {
                DFSLog.i("Fusion: logout succeeded")
                it.resume(true)
            } else {
                DFSLog.e("Fusion: logout failed")
                it.resume(false)
            }
        }, timeoutMax)
    }
}