package com.github.gotify.service

import android.app.AlarmManager
import android.app.Notification
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.graphics.Color
import android.net.ConnectivityManager
import android.net.Network
import android.net.Uri
import android.os.Build
import android.os.IBinder
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import com.github.gotify.MarkwonFactory
import com.github.gotify.MissedMessageUtil
import com.github.gotify.NotificationSupport
import com.github.gotify.R
import com.github.gotify.Settings
import com.github.gotify.Utils
import com.github.gotify.api.Callback
import com.github.gotify.api.ClientFactory
import com.github.gotify.client.api.ApplicationApi
import com.github.gotify.client.api.MessageApi
import com.github.gotify.client.model.Application
import com.github.gotify.client.model.Message
import com.github.gotify.log.Log
import com.github.gotify.log.UncaughtExceptionHandler
import com.github.gotify.messages.Extras
import com.github.gotify.messages.MessagesActivity
import com.github.gotify.picasso.PicassoHandler
import io.noties.markwon.Markwon
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicLong

internal class WebSocketService : Service() {
    companion object {
        val NEW_MESSAGE_BROADCAST = "${WebSocketService::class.java.name}.NEW_MESSAGE"
        private const val NOT_LOADED = -2L
    }

    private lateinit var settings: Settings
    private var connection: WebSocketConnection? = null
    private val networkCallback: ConnectivityManager.NetworkCallback =
        object : ConnectivityManager.NetworkCallback() {
            override fun onAvailable(network: Network) {
                super.onAvailable(network)
                Log.i("WebSocket: Network available, reconnect if needed.")
                connection?.start()
            }
        }
    private val appIdToApp = ConcurrentHashMap<Long, Application>()

    private val lastReceivedMessage = AtomicLong(NOT_LOADED)
    private lateinit var missingMessageUtil: MissedMessageUtil

    private lateinit var picassoHandler: PicassoHandler
    private lateinit var markwon: Markwon

    override fun onCreate() {
        super.onCreate()
        settings = Settings(this)
        val client = ClientFactory.clientToken(
            settings.url,
            settings.sslSettings(),
            settings.token
        )
        missingMessageUtil = MissedMessageUtil(client.createService(MessageApi::class.java))
        Log.i("Create ${javaClass.simpleName}")
        picassoHandler = PicassoHandler(this, settings)
        markwon = MarkwonFactory.createForNotification(this, picassoHandler.get())
    }

    override fun onDestroy() {
        super.onDestroy()
        connection?.close()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            (getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager)
                .unregisterNetworkCallback(networkCallback)
        }

        Log.w("Destroy ${javaClass.simpleName}")
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.init(this)
        if (connection != null) {
            connection!!.close()
        }
        Log.i("Starting ${javaClass.simpleName}")
        super.onStartCommand(intent, flags, startId)
        Thread { startPushService() }.start()

        return START_STICKY
    }

    private fun startPushService() {
        UncaughtExceptionHandler.registerCurrentThread()
        showForegroundNotification(getString(R.string.websocket_init))

        if (lastReceivedMessage.get() == NOT_LOADED) {
            missingMessageUtil.lastReceivedMessage { lastReceivedMessage.set(it) }
        }

        val cm = getSystemService(CONNECTIVITY_SERVICE) as ConnectivityManager
        val alarmManager = getSystemService(ALARM_SERVICE) as AlarmManager

        connection = WebSocketConnection(
            settings.url,
            settings.sslSettings(),
            settings.token,
            cm,
            alarmManager
        )
            .onOpen { onOpen() }
            .onClose { onClose() }
            .onBadRequest { message -> onBadRequest(message) }
            .onNetworkFailure { minutes -> onNetworkFailure(minutes) }
            .onMessage { message -> onMessage(message) }
            .onReconnected { notifyMissedNotifications() }
            .start()
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            cm.registerDefaultNetworkCallback(networkCallback)
        }
        fetchApps()
    }

    private fun fetchApps() {
        ClientFactory.clientToken(settings.url, settings.sslSettings(), settings.token)
            .createService(ApplicationApi::class.java)
            .apps
            .enqueue(
                Callback.call(
                    onSuccess = Callback.SuccessBody { apps ->
                        appIdToApp.clear()
                        appIdToApp.putAll(apps.associateBy { it.id })
                        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                            NotificationSupport.createChannels(
                                this,
                                (this.getSystemService(NOTIFICATION_SERVICE) as NotificationManager),
                                apps
                            )
                        }
                    },
                    onError = { appIdToApp.clear() }
                )
            )
    }

    private fun onClose() {
        showForegroundNotification(
            getString(R.string.websocket_closed),
            getString(R.string.websocket_reconnect)
        )
        ClientFactory.userApiWithToken(settings)
            .currentUser()
            .enqueue(
                Callback.call(
                    onSuccess = { doReconnect() },
                    onError = { exception ->
                        if (exception.code == 401) {
                            showForegroundNotification(
                                getString(R.string.user_action),
                                getString(R.string.websocket_closed_logout)
                            )
                        } else {
                            Log.i(
                                "WebSocket closed but the user still authenticated, " +
                                    "trying to reconnect"
                            )
                            doReconnect()
                        }
                    }
                )
            )
    }

    private fun doReconnect() {
        if (connection == null) {
            return
        }
        connection!!.scheduleReconnect(15)
    }

    private fun onBadRequest(message: String) {
        showForegroundNotification(getString(R.string.websocket_could_not_connect), message)
    }

    private fun onNetworkFailure(minutes: Int) {
        val status = getString(R.string.websocket_not_connected)
        val intervalUnit = resources
            .getQuantityString(R.plurals.websocket_retry_interval, minutes, minutes)
        showForegroundNotification(
            status,
            "${getString(R.string.websocket_reconnect)} $intervalUnit"
        )
    }

    private fun onOpen() {
        showForegroundNotification(getString(R.string.websocket_listening))
    }

    private fun notifyMissedNotifications() {
        val messageId = lastReceivedMessage.get()
        if (messageId == NOT_LOADED) {
            return
        }

        val messages = missingMessageUtil.missingMessages(messageId).filterNotNull()

        if (messages.size > 5) {
            onGroupedMessages(messages)
        } else {
            messages.forEach {
                onMessage(it)
            }
        }
    }

    private fun onGroupedMessages(messages: List<Message>) {
        var highestPriority = 0L
        messages.forEach { message ->
            if (lastReceivedMessage.get() < message.id) {
                lastReceivedMessage.set(message.id)
                highestPriority = highestPriority.coerceAtLeast(message.priority)
            }
            broadcast(message)
        }
        val size = messages.size
        showNotification(
            NotificationSupport.ID.GROUPED,
            getString(R.string.missed_messages),
            getString(R.string.grouped_message, size),
            highestPriority,
            null
        )
    }

    private fun onMessage(message: Message) {
        if (lastReceivedMessage.get() < message.id) {
            lastReceivedMessage.set(message.id)
        }
        broadcast(message)
        showNotification(
            message.id,
            message.title,
            message.message,
            message.priority,
            message.extras,
            message.appid
        )
    }

    private fun broadcast(message: Message) {
        val intent = Intent()
        intent.action = NEW_MESSAGE_BROADCAST
        intent.putExtra("message", Utils.JSON.toJson(message))
        sendBroadcast(intent)
    }

    override fun onBind(intent: Intent): IBinder? = null

    private fun showForegroundNotification(title: String, message: String? = null) {
        val notificationIntent = Intent(this, MessagesActivity::class.java)

        val pendingIntent = PendingIntent.getActivity(
            this,
            0,
            notificationIntent,
            PendingIntent.FLAG_IMMUTABLE
        )
        val notificationBuilder =
            NotificationCompat.Builder(this, NotificationSupport.Channel.FOREGROUND)
        notificationBuilder.setSmallIcon(R.drawable.ic_gotify)
        notificationBuilder.setOngoing(true)
        notificationBuilder.priority = NotificationCompat.PRIORITY_MIN
        notificationBuilder.setShowWhen(false)
        notificationBuilder.setWhen(0)
        notificationBuilder.setContentTitle(title)

        if (message != null) {
            notificationBuilder.setContentText(message)
            notificationBuilder.setStyle(NotificationCompat.BigTextStyle().bigText(message))
        }

        notificationBuilder.setContentIntent(pendingIntent)
        notificationBuilder.color = ContextCompat.getColor(applicationContext, R.color.colorPrimary)
        startForeground(NotificationSupport.ID.FOREGROUND, notificationBuilder.build())
    }

    private fun showNotification(
        id: Int,
        title: String,
        message: String,
        priority: Long,
        extras: Map<String, Any>?
    ) {
        showNotification(id.toLong(), title, message, priority, extras, -1L)
    }

    private fun showNotification(
        id: Long,
        title: String,
        message: String,
        priority: Long,
        extras: Map<String, Any>?,
        appId: Long
    ) {
        var intent: Intent

        val intentUrl = Extras.getNestedValue(
            String::class.java,
            extras,
            "android::action",
            "onReceive",
            "intentUrl"
        )

        if (intentUrl != null) {
            intent = Intent(Intent.ACTION_VIEW)
            intent.data = Uri.parse(intentUrl)
            intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK
            startActivity(intent)
        }

        val url = Extras.getNestedValue(
            String::class.java,
            extras,
            "client::notification",
            "click",
            "url"
        )

        if (url != null) {
            intent = Intent(Intent.ACTION_VIEW)
            intent.data = Uri.parse(url)
        } else {
            intent = Intent(this, MessagesActivity::class.java)
        }

        val contentIntent = PendingIntent.getActivity(
            this,
            0,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )

        val channelId: String
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
            NotificationSupport.areAppChannelsRequested(this)
        ) {
            channelId = NotificationSupport.getChannelID(priority, appId.toString())
            NotificationSupport.createChannelIfNonexistent(
                this,
                appId.toString(),
                channelId
            )
        } else {
            channelId = NotificationSupport.convertPriorityToChannel(priority)
        }

        val b = NotificationCompat.Builder(this, channelId)

        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
            showNotificationGroup(channelId)
        }

        b.setAutoCancel(true)
            .setDefaults(Notification.DEFAULT_ALL)
            .setWhen(System.currentTimeMillis())
            .setSmallIcon(R.drawable.ic_gotify)
            .setLargeIcon(picassoHandler.getIcon(appIdToApp[appId]))
            .setTicker("${getString(R.string.app_name)} - $title")
            .setGroup(NotificationSupport.Group.MESSAGES)
            .setContentTitle(title)
            .setDefaults(Notification.DEFAULT_LIGHTS or Notification.DEFAULT_SOUND)
            .setLights(Color.CYAN, 1000, 5000)
            .setColor(ContextCompat.getColor(applicationContext, R.color.colorPrimary))
            .setContentIntent(contentIntent)

        var formattedMessage = message as CharSequence
        var newMessage: String? = null
        if (Extras.useMarkdown(extras)) {
            formattedMessage = markwon.toMarkdown(message)
            newMessage = formattedMessage.toString()
        }
        b.setContentText(newMessage ?: message)
        b.setStyle(NotificationCompat.BigTextStyle().bigText(formattedMessage))

        val notificationImageUrl = Extras.getNestedValue(
            String::class.java,
            extras,
            "client::notification",
            "bigImageUrl"
        )

        if (notificationImageUrl != null) {
            try {
                b.setStyle(
                    NotificationCompat.BigPictureStyle()
                        .bigPicture(picassoHandler.getImageFromUrl(notificationImageUrl))
                )
            } catch (e: Exception) {
                Log.e("Error loading bigImageUrl", e)
            }
        }
        val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.notify(Utils.longToInt(id), b.build())
    }

    @RequiresApi(Build.VERSION_CODES.N)
    fun showNotificationGroup(channelId: String) {
        val intent = Intent(this, MessagesActivity::class.java)
        val contentIntent = PendingIntent.getActivity(
            this,
            0,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
        )

        val builder = NotificationCompat.Builder(
            this,
            channelId
        )

        builder.setAutoCancel(true)
            .setDefaults(Notification.DEFAULT_ALL)
            .setWhen(System.currentTimeMillis())
            .setSmallIcon(R.drawable.ic_gotify)
            .setTicker(getString(R.string.app_name))
            .setGroup(NotificationSupport.Group.MESSAGES)
            .setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_CHILDREN)
            .setContentTitle(getString(R.string.grouped_notification_text))
            .setGroupSummary(true)
            .setContentText(getString(R.string.grouped_notification_text))
            .setColor(ContextCompat.getColor(applicationContext, R.color.colorPrimary))
            .setContentIntent(contentIntent)

        val notificationManager = this.getSystemService(NOTIFICATION_SERVICE) as NotificationManager
        notificationManager.notify(-5, builder.build())
    }
}
