package com.unciv.app

import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.Uri
import android.os.Build
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationCompat.DEFAULT_VIBRATE
import androidx.core.app.NotificationManagerCompat
import androidx.work.*
import com.badlogic.gdx.backends.android.AndroidApplication
import com.unciv.logic.GameInfo
import com.unciv.logic.GameSaver
import com.unciv.models.metadata.GameSettings
import com.unciv.logic.multiplayer.OnlineMultiplayer
import java.io.FileNotFoundException
import java.io.PrintWriter
import java.io.StringWriter
import java.io.Writer
import java.util.*
import java.util.concurrent.TimeUnit


class MultiplayerTurnCheckWorker(appContext: Context, workerParams: WorkerParameters)
    : Worker(appContext, workerParams) {

    companion object {
        const val WORK_TAG = "UNCIV_MULTIPLAYER_TURN_CHECKER_WORKER"
        const val CLIPBOARD_EXTRA = "CLIPBOARD_STRING"
        const val NOTIFICATION_ID_SERVICE = 1
        const val NOTIFICATION_ID_INFO = 2

        // Notification Channels can't be modified after creation.
        // Therefore Unciv needs to create new ones and delete previously used ones.
        // Add old channel names here when replacing them with new ones below.
        private val HISTORIC_NOTIFICATION_CHANNELS = arrayOf("UNCIV_NOTIFICATION_CHANNEL_SERVICE")

        private const val NOTIFICATION_CHANNEL_ID_INFO = "UNCIV_NOTIFICATION_CHANNEL_INFO"
        private const val NOTIFICATION_CHANNEL_ID_SERVICE = "UNCIV_NOTIFICATION_CHANNEL_SERVICE_02"

        private const val FAIL_COUNT = "FAIL_COUNT"
        private const val GAME_ID = "GAME_ID"
        private const val GAME_NAME = "GAME_NAME"
        private const val USER_ID = "USER_ID"
        private const val CONFIGURED_DELAY = "CONFIGURED_DELAY"
        private const val PERSISTENT_NOTIFICATION_ENABLED = "PERSISTENT_NOTIFICATION_ENABLED"

        fun enqueue(appContext: Context,
                    delayInMinutes: Int, inputData: Data) {

            val constraints = Constraints.Builder()
                    // If no internet is available, worker waits before becoming active.
                    .setRequiredNetworkType(NetworkType.CONNECTED)
                    .build()

            val checkTurnWork = OneTimeWorkRequestBuilder<MultiplayerTurnCheckWorker>()
                    .setConstraints(constraints)
                    .setInitialDelay(delayInMinutes.toLong(), TimeUnit.MINUTES)
                    .addTag(WORK_TAG)
                    .setInputData(inputData)
                    .build()

            WorkManager.getInstance(appContext).enqueue(checkTurnWork)
        }

        /**
         * Notification Channel for 'It's your turn' and error notifications.
         *
         * This code is necessary for API level >= 26
         * API level < 26 does not support Notification Channels
         * For more infos: https://developer.android.com/training/notify-user/channels.html#CreateChannel
         */
        fun createNotificationChannelInfo(appContext: Context) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
            val name = appContext.resources.getString(R.string.Notify_ChannelInfo_Short)
            val descriptionText = appContext.resources.getString(R.string.Notify_ChannelInfo_Long)
            val importance = NotificationManager.IMPORTANCE_HIGH
            val mChannel = NotificationChannel(NOTIFICATION_CHANNEL_ID_INFO, name, importance)
            mChannel.description = descriptionText
            mChannel.setShowBadge(true)
            mChannel.lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC

            val notificationManager = appContext.getSystemService(AndroidApplication.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(mChannel)
        }

        /**
         * Notification Channel for persistent service notification.
         *
         * This code is necessary for API level >= 26
         * API level < 26 does not support Notification Channels
         * For more infos: https://developer.android.com/training/notify-user/channels.html#CreateChannel
         */
        fun createNotificationChannelService(appContext: Context) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
            val name = appContext.resources.getString(R.string.Notify_ChannelService_Short)
            val descriptionText = appContext.resources.getString(R.string.Notify_ChannelService_Long)
            val importance = NotificationManager.IMPORTANCE_MIN
            val mChannel = NotificationChannel(NOTIFICATION_CHANNEL_ID_SERVICE, name, importance)
            mChannel.setShowBadge(false)
            mChannel.lockscreenVisibility = NotificationCompat.VISIBILITY_PUBLIC
            mChannel.description = descriptionText

            val notificationManager = appContext.getSystemService(AndroidApplication.NOTIFICATION_SERVICE) as NotificationManager
            notificationManager.createNotificationChannel(mChannel)
        }

        /**
         * The persistent notification is purely for informational reasons.
         * It is not technically necessary for the Worker, since it is not a Service.
         */
        fun showPersistentNotification(appContext: Context, lastTimeChecked: String, checkPeriod: String) {
            val pendingIntent: PendingIntent =
                    Intent(appContext, AndroidLauncher::class.java).let { notificationIntent ->
                        PendingIntent.getActivity(appContext, 0, notificationIntent, 0)
                    }

            val notification: NotificationCompat.Builder = NotificationCompat.Builder(appContext, NOTIFICATION_CHANNEL_ID_SERVICE)
                    .setPriority(NotificationManagerCompat.IMPORTANCE_MIN) // it's only a status
                    .setContentTitle(appContext.resources.getString(R.string.Notify_Persist_Short) + " " + lastTimeChecked)
                    .setStyle(NotificationCompat.BigTextStyle()
                            .bigText(appContext.resources.getString(R.string.Notify_Persist_Long_P1) + " " +
                                    appContext.resources.getString(R.string.Notify_Persist_Long_P2) + " " + checkPeriod + " "
                                    + appContext.resources.getString(R.string.Notify_Persist_Long_P3)
                                    + " " + appContext.resources.getString(R.string.Notify_Persist_Long_P4)))
                    .setSmallIcon(R.drawable.uncivicon2)
                    .setContentIntent(pendingIntent)
                    .setCategory(NotificationCompat.CATEGORY_SERVICE)
                    .setOnlyAlertOnce(true)
                    .setOngoing(true)
                    .setShowWhen(false)

            with(NotificationManagerCompat.from(appContext)) {
                notify(NOTIFICATION_ID_INFO, notification.build())
            }
        }

        fun notifyUserAboutTurn(applicationContext: Context, game: Pair<String, String>) {
            val intent = Intent(applicationContext, AndroidLauncher::class.java).apply {
                action = Intent.ACTION_VIEW
                data = Uri.parse("https://unciv.app/multiplayer?id=${game.second}")
            }
            val pendingIntent = PendingIntent.getActivity(applicationContext, 0, intent, 0)

            val contentTitle = applicationContext.resources.getString(R.string.Notify_YourTurn_Short)
            val notification: NotificationCompat.Builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID_INFO)
                    .setPriority(NotificationManagerCompat.IMPORTANCE_HIGH) // people are waiting!
                    .setContentTitle(contentTitle)
                    .setContentText(applicationContext.resources.getString(R.string.Notify_YourTurn_Long).replace("[gameName]", game.first))
                    .setTicker(contentTitle)
                    // without at least vibrate, some Android versions don't show a heads-up notification
                    .setDefaults(DEFAULT_VIBRATE)
                    .setLights(Color.YELLOW, 300, 100)
                    .setSmallIcon(R.drawable.uncivicon2)
                    .setContentIntent(pendingIntent)
                    .setCategory(NotificationCompat.CATEGORY_SOCIAL)
                    .setOngoing(false)

            with(NotificationManagerCompat.from(applicationContext)) {
                notify(NOTIFICATION_ID_INFO, notification.build())
            }
        }

        fun startTurnChecker(applicationContext: Context, currentGameInfo: GameInfo, settings: GameSettings) {
            val gameFiles = GameSaver.getSaves(true)
            val gameIds = Array(gameFiles.count()) {""}
            val gameNames = Array(gameFiles.count()) {""}

            var count = 0
            for (gameFile in gameFiles) {
                try {
                    val gamePreview = GameSaver.loadGamePreviewFromFile(gameFile)
                    if (gamePreview.turnNotification) {
                        gameIds[count] = gamePreview.gameId
                        gameNames[count] = gameFile.name()
                        count++
                    }
                } catch (ex: Throwable) {
                    //only loadGamePreviewFromFile can throw an exception
                    //nothing will be added to the arrays if it fails
                    //just skip one file
                }
            }

            if (currentGameInfo.currentPlayerCiv.playerId == settings.userId) {
                // May be useful to remind a player that he forgot to complete his turn.
                val gameIndex = gameIds.indexOf(currentGameInfo.gameId)
                // Of the turnNotification is OFF, this will be -1 since we never saved this game in the array
                // Or possibly reading the preview file returned an exception
                if (gameIndex!=-1)
                    notifyUserAboutTurn(applicationContext, Pair(gameNames[gameIndex], gameIds[gameIndex]))
            } else {
                val inputData = workDataOf(Pair(FAIL_COUNT, 0), Pair(GAME_ID, gameIds), Pair(GAME_NAME, gameNames),
                        Pair(USER_ID, settings.userId), Pair(CONFIGURED_DELAY, settings.multiplayerTurnCheckerDelayInMinutes),
                        Pair(PERSISTENT_NOTIFICATION_ENABLED, settings.multiplayerTurnCheckerPersistentNotificationEnabled))

                if (settings.multiplayerTurnCheckerPersistentNotificationEnabled) {
                    showPersistentNotification(applicationContext,
                            "—", settings.multiplayerTurnCheckerDelayInMinutes.toString())
                }
                // Initial check always happens after a minute, ignoring delay config. Better user experience this way.
                enqueue(applicationContext, 1, inputData)
            }
        }

        /**
         * Necessary for Multiplayer Turner Checker, starting with Android Oreo
         */
        fun createNotificationChannels(appContext: Context) {
            createNotificationChannelInfo(appContext)
            createNotificationChannelService(appContext)
            destroyOldChannels(appContext)
        }

        /**
         *  Notification Channels can't be modified after creation.
         *  Therefore Unciv needs to create new ones and delete legacy ones.
         */
        private fun destroyOldChannels(appContext: Context) {
            if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
            val notificationManager = appContext.getSystemService(AndroidApplication.NOTIFICATION_SERVICE) as NotificationManager
            HISTORIC_NOTIFICATION_CHANNELS.forEach {
                if (null != notificationManager.getNotificationChannel(it)) {
                    notificationManager.deleteNotificationChannel(it)
                }
            }
        }
    }

    override fun doWork(): Result {
        val showPersistNotific = inputData.getBoolean(PERSISTENT_NOTIFICATION_ENABLED, true)
        val configuredDelay = inputData.getInt(CONFIGURED_DELAY, 5)

        try {
            val gameIds = inputData.getStringArray(GAME_ID)!!
            val gameNames = inputData.getStringArray(GAME_NAME)!!
            var arrayIndex = 0
            // We only want to notify the user or update persisted notification once but still want
            // to download all games to update the files so we save the first one we find
            var foundGame: Pair<String, String>? = null

            for (gameId in gameIds){
                //gameId could be an empty string if startTurnChecker fails to load all files
                if (gameId.isEmpty())
                    continue

                try {
                    val gamePreview = OnlineMultiplayer().tryDownloadGamePreview(gameId)
                    val currentTurnPlayer = gamePreview.getCivilization(gamePreview.currentPlayer)

                    //Save game so MultiplayerScreen gets updated
                    /*
                    I received multiple reports regarding broken save games.
                    All of them where missing a few thousand chars at the end of the save game.
                    I assume this happened because the TurnCheckerWorker gets canceled by the AndroidLauncher
                    while saves are getting saved right here.
                    Lets hope it works with gamePreview as they are a lot smaller and faster to save
                     */
                    GameSaver.saveGame(gamePreview, gameNames[arrayIndex])

                    if (currentTurnPlayer.playerId == inputData.getString(USER_ID)!! && foundGame == null) {
                        foundGame = Pair(gameNames[arrayIndex], gameIds[arrayIndex])
                    }
                    arrayIndex++
                } catch (ex: FileNotFoundException){
                    // FileNotFoundException is thrown by OnlineMultiplayer().tryDownloadGamePreview(gameId)
                    // and indicates that there is no game preview present for this game
                    // in the dropbox so we should not check for this game in the future anymore
                    val currentGamePreview = GameSaver.loadGamePreviewByName(gameNames[arrayIndex])
                    currentGamePreview.turnNotification = false
                    GameSaver.saveGame(currentGamePreview, gameNames[arrayIndex])
                }
            }

            if (foundGame != null){
                notifyUserAboutTurn(applicationContext, foundGame)
                with(NotificationManagerCompat.from(applicationContext)) {
                    cancel(NOTIFICATION_ID_SERVICE)
                }
            } else {
                if (showPersistNotific) { updatePersistentNotification(inputData) }
                // We have to reset the fail counter since no exception appeared
                val inputDataFailReset = Data.Builder().putAll(inputData).putInt(FAIL_COUNT, 0).build()
                enqueue(applicationContext, configuredDelay, inputDataFailReset)
            }

        } catch (ex: Exception) {
            val failCount = inputData.getInt(FAIL_COUNT, 0)
            if (failCount > 3) {
                showErrorNotification(getStackTraceString(ex))
                with(NotificationManagerCompat.from(applicationContext)) {
                    cancel(NOTIFICATION_ID_SERVICE)
                }
                return Result.failure()
            } else {
                if (showPersistNotific) { showPersistentNotification(applicationContext,
                        applicationContext.resources.getString(R.string.Notify_Error_Retrying), configuredDelay.toString()) }
                // If check fails, retry in one minute.
                // Makes sense, since checks only happen if Internet is available in principle.
                // Therefore a failure means either a problem with the GameInfo or with Dropbox.
                val inputDataFailIncrease = Data.Builder().putAll(inputData).putInt(FAIL_COUNT, failCount + 1).build()
                enqueue(applicationContext, 1, inputDataFailIncrease)
            }
        } catch (outOfMemory: OutOfMemoryError){ // no point in trying multiple times if this was an oom error
            return Result.failure()
        }
        return Result.success()
    }

    private fun getStackTraceString(ex: Exception): String {
        val writer: Writer = StringWriter()
        ex.printStackTrace(PrintWriter(writer))
        return writer.toString()
    }

    private fun updatePersistentNotification(inputData: Data) {
        val cal = GregorianCalendar.getInstance()
        val hour = cal.get(GregorianCalendar.HOUR_OF_DAY).toString()
        var minute = cal.get(GregorianCalendar.MINUTE).toString()
        if (minute.length == 1) {
            minute = "0$minute"
        }
        val displayTime = "$hour:$minute"

        showPersistentNotification(applicationContext, displayTime,
                inputData.getInt(CONFIGURED_DELAY, 5).toString())
    }

    private fun showErrorNotification(stackTraceString: String) {
        val pendingLaunchGameIntent: PendingIntent =
                Intent(applicationContext, AndroidLauncher::class.java).let { notificationIntent ->
                    PendingIntent.getActivity(applicationContext, 0, notificationIntent, 0)
                }

        val pendingCopyClipboardIntent: PendingIntent =
                Intent(applicationContext, CopyToClipboardReceiver::class.java).putExtra(CLIPBOARD_EXTRA, stackTraceString)
                        .let { notificationIntent -> PendingIntent.getBroadcast(applicationContext,0, notificationIntent, 0)
                }

        val notification: NotificationCompat.Builder = NotificationCompat.Builder(applicationContext, NOTIFICATION_CHANNEL_ID_INFO)
                .setPriority(NotificationManagerCompat.IMPORTANCE_DEFAULT) // No direct user action expected
                .setContentTitle(applicationContext.resources.getString(R.string.Notify_Error_Short))
                .setContentText(applicationContext.resources.getString(R.string.Notify_Error_Long))
                .setSmallIcon(R.drawable.uncivicon2)
                // without at least vibrate, some Android versions don't show a heads-up notification
                .setDefaults(DEFAULT_VIBRATE)
                .setLights(Color.YELLOW, 300, 100)
                .setContentIntent(pendingLaunchGameIntent)
                .setCategory(NotificationCompat.CATEGORY_ERROR)
                .setOngoing(false)
                .addAction(0, applicationContext.resources.getString(R.string.Notify_Error_CopyAction), pendingCopyClipboardIntent)

        with(NotificationManagerCompat.from(applicationContext)) {
            notify(NOTIFICATION_ID_INFO, notification.build())
        }
    }
}