package ac.mdiq.podcini.net.sync.wifi

import ac.mdiq.podcini.R
import ac.mdiq.podcini.net.sync.LockingAsyncExecutor
import ac.mdiq.podcini.net.sync.LockingAsyncExecutor.executeLockedAsync
import ac.mdiq.podcini.net.sync.SyncService
import ac.mdiq.podcini.net.sync.SynchronizationSettings
import ac.mdiq.podcini.net.sync.model.EpisodeAction
import ac.mdiq.podcini.net.sync.model.EpisodeAction.Companion.readFromJsonObject
import ac.mdiq.podcini.net.sync.model.EpisodeActionChanges
import ac.mdiq.podcini.net.sync.model.ISyncService
import ac.mdiq.podcini.net.sync.model.SubscriptionChanges
import ac.mdiq.podcini.net.sync.model.SyncServiceException
import ac.mdiq.podcini.net.sync.model.UploadChangesResponse
import ac.mdiq.podcini.storage.database.Episodes.getEpisodeByGuidOrUrl
import ac.mdiq.podcini.storage.database.Episodes.getEpisodes
import ac.mdiq.podcini.storage.database.Episodes.hasAlmostEnded
import ac.mdiq.podcini.storage.database.RealmDB.upsertBlk
import ac.mdiq.podcini.storage.model.Episode
import ac.mdiq.podcini.storage.utils.EpisodeFilter
import ac.mdiq.podcini.storage.utils.EpisodeSortOrder
import ac.mdiq.podcini.storage.utils.EpisodeState
import ac.mdiq.podcini.storage.utils.Rating
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.Logd
import ac.mdiq.podcini.util.Logs
import android.content.Context
import androidx.core.content.ContextCompat.getString
import androidx.work.BackoffPolicy
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import androidx.work.WorkerParameters
import kotlinx.coroutines.delay
import java.io.BufferedReader
import java.io.InputStreamReader
import java.io.PrintWriter
import java.net.BindException
import java.net.ConnectException
import java.net.ServerSocket
import java.net.Socket
import java.net.SocketTimeoutException
import java.util.Date
import java.util.concurrent.TimeUnit
import org.apache.commons.lang3.StringUtils
import org.json.JSONArray
import kotlin.math.min

class WifiSyncService(val context: Context, params: WorkerParameters) : SyncService(context, params), ISyncService {

    private var loginFail = false

    override suspend fun doWork(): Result {
        Logd(TAG, "doWork() called")

        SynchronizationSettings.updateLastSynchronizationAttempt()
        setCurrentlyActive(true)

        login()

        if (socket != null && !loginFail) {
            if (isGuest) {
                delay(1000)
//                TODO: not using lastSync
                val lastSync = SynchronizationSettings.lastEpisodeActionSynchronizationTimestamp
                val newTimeStamp = pushEpisodeActions(this, 0L, System.currentTimeMillis())
                SynchronizationSettings.setLastEpisodeActionSynchronizationAttemptTimestamp(newTimeStamp)
                EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "50"))
                sendToPeer("AllSent", "AllSent")

                var receivedBye = false
                while (!receivedBye) {
                    try { receivedBye = receiveFromPeer()
                    } catch (e: SocketTimeoutException) {
                        Logs("Guest", e, getString(context, R.string.sync_error_host_not_respond))
                        logout()
                        EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "100"))
                        EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_error, getString(context, R.string.sync_error_host_not_respond)))
                        SynchronizationSettings.setLastSynchronizationAttemptSuccess(false)
                        return Result.failure()
                    }
                }
            } else {
                var receivedBye = false
                while (!receivedBye) {
                    try { receivedBye = receiveFromPeer()
                    } catch (e: SocketTimeoutException) {
                        Logs("Host", e, getString(context, R.string.sync_error_guest_not_respond))
                        logout()
                        EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "100"))
                        EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_error, getString(context, R.string.sync_error_guest_not_respond)))
                        SynchronizationSettings.setLastSynchronizationAttemptSuccess(false)
                        return Result.failure()
                    }
                }
                EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "50"))
                //                TODO: not using lastSync
                val lastSync = SynchronizationSettings.lastEpisodeActionSynchronizationTimestamp
                val newTimeStamp = pushEpisodeActions(this, 0L, System.currentTimeMillis())
                SynchronizationSettings.setLastEpisodeActionSynchronizationAttemptTimestamp(newTimeStamp)
                sendToPeer("AllSent", "AllSent")
            }
        } else {
            logout()
            EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "100"))
            EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_error, "Login failure"))
            SynchronizationSettings.setLastSynchronizationAttemptSuccess(false)
            return Result.failure()
        }

        logout()
        EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "100"))
        EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_success))
        SynchronizationSettings.setLastSynchronizationAttemptSuccess(true)
        return Result.success()
    }

    private var socket: Socket? = null

     override fun login() {
        Logd(TAG, "serverIp: $hostIp serverPort: $hostPort $isGuest")
        EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "2"))
        if (!isPortInUse(hostPort)) {
            if (isGuest) {
                val maxTries = 120
                var numTries = 0
                while (numTries < maxTries) {
                    try {
                        socket = Socket(hostIp, hostPort)
                        break
                    } catch (e: ConnectException) { Thread.sleep(1000) }
                    numTries++
                }
                if (numTries >= maxTries) loginFail = true
                if (socket != null) {
                    sendToPeer("Hello", "Hello, Server!")
                    receiveFromPeer()
                }
            } else {
                try {
                    if (serverSocket == null) serverSocket = ServerSocket(hostPort)
                    serverSocket!!.soTimeout = 120000
                    try {
                        socket = serverSocket!!.accept()
                        while (true) {
                            Logd(TAG, "waiting for guest message")
                            try {
                                receiveFromPeer()
                                sendToPeer("Hello", "Hello, Client")
                                break
                            } catch (e: SocketTimeoutException) {
                                Logs("Server", e, "Guest not responding in 120 seconds, giving up")
                                loginFail = true
                                break
                            }
                        }
                    } catch (e: Exception) {
                        Logs("Server", e, "No guest connecing in 120 seconds, giving up")
                        loginFail = true
                    }
                } catch (e: BindException) {
                    Logs("Server", e, "Failed to start server: Port $hostPort already in use")
                    loginFail = true
                }
            }
        } else {
            Logd(TAG, "port $hostPort in use, ignored")
            loginFail = true
        }
        EventFlow.postEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_in_progress, "5"))
    }

     private fun isPortInUse(port: Int): Boolean {
        val command = "netstat -tlnp"
        val process = Runtime.getRuntime().exec(command)
        val output = process.inputStream.bufferedReader().use { it.readText() }
//        Logd(TAG, "isPortInUse: $output")
        return output.contains(":$port") // Check if output contains the port
    }

    private fun sendToPeer(messageType: String, message: String) {
        val writer = PrintWriter(socket!!.getOutputStream(), true)
        writer.println("$messageType|$message")
    }

    @Throws(SocketTimeoutException::class)
    private fun receiveFromPeer() : Boolean {
        val reader = BufferedReader(InputStreamReader(socket!!.getInputStream()))
        val message: String?
        socket!!.soTimeout = 120000
        try { message = reader.readLine() } catch (e: SocketTimeoutException) { throw e }
        if (message != null) {
            val parts = message.split("|")
            if (parts.size == 2) {
                val messageType = parts[0]
                val messageData = parts[1]
                // Process the message based on the type
                when (messageType) {
                    "Hello" -> Logd(TAG, "Received Hello message: $messageData")
                    "EpisodeActions" -> {
                        val remoteActions = mutableListOf<EpisodeAction>()
                        val jsonArray = JSONArray(messageData)
                        for (i in 0 until jsonArray.length()) {
                            val jsonAction = jsonArray.getJSONObject(i)

                            Logd(TAG, "Received EpisodeActions message: $i $jsonAction")
                            val action = readFromJsonObject(jsonAction)
                            if (action != null) remoteActions.add(action)
                        }
                        processEpisodeActions(remoteActions)
                    }
                    "AllSent" -> {
                        Logd(TAG, "Received AllSent message: $messageData")
                        return true
                    }
                    else -> Logd(TAG, "Received unknown message: $messageData")
                }
            }
        }
        return false
    }

    @Throws(SyncServiceException::class)
    override fun getSubscriptionChanges(lastSync: Long): SubscriptionChanges? {
        Logd(TAG, "getSubscriptionChanges does nothing")
        return null
    }

    @Throws(SyncServiceException::class)
    override fun uploadSubscriptionChanges(added: List<String>, removed: List<String>): UploadChangesResponse? {
        Logd(TAG, "uploadSubscriptionChanges does nothing")
        return null
    }

    @Throws(SyncServiceException::class)
    override fun getEpisodeActionChanges(lastSync: Long): EpisodeActionChanges? {
        Logd(TAG, "getEpisodeActionChanges does nothing")
        return null
    }

    override fun pushEpisodeActions(syncServiceImpl: ISyncService, lastSync: Long, newTimeStamp_: Long): Long {
        var newTimeStamp = newTimeStamp_
        EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_episodes_upload))
        val queuedEpisodeActions: MutableList<EpisodeAction> = synchronizationQueueStorage.queuedEpisodeActions
        Logd(TAG, "pushEpisodeActions queuedEpisodeActions: ${queuedEpisodeActions.size}")

        if (lastSync == 0L) {
            EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_upload_played))
//            only push downloaded items
            val pausedItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.paused.name), EpisodeSortOrder.DATE_NEW_OLD)
            val readItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.played.name), EpisodeSortOrder.DATE_NEW_OLD)
            val favoriteItems = getEpisodes(0, Int.MAX_VALUE, EpisodeFilter(EpisodeFilter.States.superb.name), EpisodeSortOrder.DATE_NEW_OLD)
            val comItems = mutableSetOf<Episode>()
            comItems.addAll(pausedItems)
            comItems.addAll(readItems)
            comItems.addAll(favoriteItems)
            Logd(TAG, "First sync. Upload state for all " + comItems.size + " played episodes")
            for (item in comItems) {
                val played = EpisodeAction.Builder(item, EpisodeAction.PLAY)
                    .timestamp(Date(item.lastPlayedTime))
                    .started(item.startPosition / 1000)
                    .position(item.position / 1000)
                    .playedDuration(item.playedDuration / 1000)
                    .total(item.duration / 1000)
                    .isFavorite(item.isSUPER)
                    .playState(item.playState)
                    .build()
                queuedEpisodeActions.add(played)
            }
        }
        if (queuedEpisodeActions.isNotEmpty()) {
            LockingAsyncExecutor.lock.lock()
            try {
                Logd(TAG, "Uploading ${queuedEpisodeActions.size} actions: ${StringUtils.join(queuedEpisodeActions, ", ")}")
                val postResponse = uploadEpisodeActions(queuedEpisodeActions)
                newTimeStamp = postResponse.timestamp
                Logd(TAG, "Upload episode response: $postResponse")
                synchronizationQueueStorage.clearEpisodeActionQueue()
            } finally { LockingAsyncExecutor.lock.unlock() }
        }
        return newTimeStamp
    }

    @Throws(SyncServiceException::class)
    override fun uploadEpisodeActions(queuedEpisodeActions: List<EpisodeAction>): UploadChangesResponse {
//        Logd(TAG, "uploadEpisodeActions called")
        var i = 0
        while (i < queuedEpisodeActions.size) {
            uploadEpisodeActionsPartial(queuedEpisodeActions, i, min(queuedEpisodeActions.size, (i + UPLOAD_BULK_SIZE)))
            i += UPLOAD_BULK_SIZE
            Thread.sleep(1000)
        }
        return WifiEpisodeActionPostResponse(System.currentTimeMillis() / 1000)
    }

    @Throws(SyncServiceException::class)
    private fun uploadEpisodeActionsPartial(queuedEpisodeActions: List<EpisodeAction>, from: Int, to: Int) {
//        Logd(TAG, "uploadEpisodeActionsPartial called")
        try {
            val list = JSONArray()
            for (i in from until to) {
                val episodeAction = queuedEpisodeActions[i]
                val obj = episodeAction.writeToJsonObject()
                if (obj != null) {
                    Logd(TAG, "sending EpisodeAction: $obj")
                    list.put(obj)
                }
            }
            sendToPeer("EpisodeActions", list.toString())
        } catch (e: Exception) {
            Logs(TAG, e)
            throw SyncServiceException(e)
        }
    }

    override fun processEpisodeAction(action: EpisodeAction): Pair<Long, Episode>? {
        val guid = if (isValidGuid(action.guid)) action.guid else null
        var feedItem = getEpisodeByGuidOrUrl(guid, action.episode, false)
        if (feedItem == null) {
            Logd(TAG, "Unknown feed item: $action")
            return null
        }
        var idRemove: Long? = null
        Logd(TAG, "processEpisodeAction ${feedItem.lastPlayedTime} ${(action.timestamp?.time?:0L)} ${action.position} ${feedItem.title}")
        if (feedItem.lastPlayedTime < (action.timestamp?.time?:0L)) {
            feedItem = upsertBlk(feedItem) {
                it.startPosition = action.started * 1000
                it.setPosition(action.position * 1000)
                it.playedDuration = action.playedDuration * 1000
                it.lastPlayedTime = (action.timestamp!!.time)
                it.rating = if (action.isFavorite) Rating.SUPER.code else Rating.UNRATED.code
                it.setPlayState(EpisodeState.fromCode(action.playState))
                if (hasAlmostEnded(it)) {
                    Logd(TAG, "Marking as played")
                    it.setPlayed(true)
                    it.setPosition(0)
                    idRemove = it.id
                } else Logd(TAG, "Setting position")
            }
            EventFlow.postEvent(FlowEvent.EpisodeEvent.updated(feedItem))
        } else Logd(TAG, "local is newer, no change")
        return if (idRemove != null) Pair(idRemove, feedItem) else null
    }

    override fun logout() {
        socket?.close()
    }

    private class WifiEpisodeActionPostResponse(epochSecond: Long) : UploadChangesResponse(epochSecond)

    companion object {
        private const val WORK_ID_SYNC = "SyncServiceWorkId"
        private const val UPLOAD_BULK_SIZE = 30

        var serverSocket:  ServerSocket? = null
        var isGuest = false
        var hostIp : String = ""
        var hostPort: Int = 54628

        private var isCurrentlyActive = false
        internal fun setCurrentlyActive(active: Boolean) {
            isCurrentlyActive = active
        }

        fun startInstantSync(context: Context, hostPort_: Int = 54628, hostIp_: String="", isGuest_: Boolean = false) {
            hostIp = hostIp_
            isGuest = isGuest_
            hostPort = hostPort_
            executeLockedAsync {
                SynchronizationSettings.resetTimestamps()
                val builder: OneTimeWorkRequest.Builder = OneTimeWorkRequest.Builder(WifiSyncService::class.java)
                    .setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 10, TimeUnit.MINUTES)

                // Give it some time, so other possible actions can be queued.
                builder.setInitialDelay(20L, TimeUnit.SECONDS)
                EventFlow.postStickyEvent(FlowEvent.SyncServiceEvent(R.string.sync_status_started))

                val workRequest: OneTimeWorkRequest = builder.setInitialDelay(0L, TimeUnit.SECONDS).build()
                WorkManager.getInstance(context).enqueueUniqueWork(hostIp_, ExistingWorkPolicy.REPLACE, workRequest)
            }
        }
    }
}