package com.sinch.android.rtc.sample.push.fcm

import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.ComponentName
import android.content.Intent
import android.content.ServiceConnection
import android.os.Build
import android.os.IBinder
import android.util.Log
import com.google.firebase.messaging.FirebaseMessagingService
import com.google.firebase.messaging.RemoteMessage
import com.sinch.android.rtc.*
import com.sinch.android.rtc.SinchPush.isSinchPushPayload
import com.sinch.android.rtc.SinchPush.queryPushNotificationPayload
import com.sinch.android.rtc.calling.CallNotificationResult
import com.sinch.android.rtc.sample.push.FcmTokenPersistence.getFcmRegistrationToken
import com.sinch.android.rtc.sample.push.FcmTokenPersistence.setFcmRegistrationToken
import com.sinch.android.rtc.sample.push.JWT.create
import com.sinch.android.rtc.sample.push.SimplePushTokenUnregistrationCallback
import com.sinch.android.rtc.sample.push.SinchService
import java.io.IOException

class FcmListenerService : FirebaseMessagingService(), PushTokenRegistrationCallback,
    UserRegistrationCallback {

    private var userId: String? = null

    override fun onMessageReceived(remoteMessage: RemoteMessage) {
        val data = remoteMessage.data
        Log.d(TAG, "Got FCM message.")
        if (!isSinchPushPayload(data)) {
            Log.d(TAG, "Non Sinch push payload received. Ignoring.")
            return
        }
        val result = try {
            queryPushNotificationPayload(applicationContext, data)
        } catch (e: Exception) {
            Log.e(TAG, "Error while executing queryPushNotificationPayload", e)
            return
        }

        // Optional: inspect the payload w/o starting Sinch Client and thus avoiding
        // onIncomingCall()
        // e.g. useful to fetch user related polices (blacklist), resources (to show a picture,
        // etc).
        Log.d(TAG, "queryPushNotificationPayload() -> display name: " + result.displayName)
        Log.d(TAG, "queryPushNotificationPayload() -> call headers: " + result.callHeaders)
        Log.d(TAG, "queryPushNotificationPayload() -> remote user ID: " + result.remoteUserId)

        // Mandatory: forward payload to the SinchClient.
        object : ServiceConnection {
            private var callNotificationResult: CallNotificationResult? = null

            override fun onServiceConnected(name: ComponentName, service: IBinder) {
                callNotificationResult?.let {
                    val sinchService = service as SinchService.SinchServiceInterface
                    try {
                        sinchService.relayRemotePushNotificationPayload(it)
                        // Optional: handle call notification result data, e.g. show a system
                        // notification or similar.
                    } catch (e: Exception) {
                        Log.e(TAG, "Error while executing relayRemotePushNotificationPayload", e)
                    }
                }
                callNotificationResult = null
            }

            override fun onServiceDisconnected(name: ComponentName) {}

            fun relayCallNotification(callNotificationResult: CallNotificationResult) {
                this.callNotificationResult = callNotificationResult
                createNotificationChannel(NotificationManager.IMPORTANCE_MAX)
                applicationContext.bindService(
                    Intent(applicationContext, SinchService::class.java), this,
                    BIND_AUTO_CREATE
                )
            }
        }.relayCallNotification(result)
    }

    override fun onNewToken(token: String) {
        super.onNewToken(token)
        Log.d(TAG, "onNewToken called with registration token: $token")
        val currentToken = getFcmRegistrationToken(this)
        if (currentToken != null && currentToken.isNotEmpty()) {
            updateFcmPushToken(currentToken, token)
        }
        setFcmRegistrationToken(this, token)
    }

    private fun createNotificationChannel(importance: Int) {
        // Create the NotificationChannel, but only on API 26+ because
        // the NotificationChannel class is new and not in the support library
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val name: CharSequence = "Sinch"
            val channel = NotificationChannel(CHANNEL_ID, name, importance).apply {
                description = "Incoming Sinch Push Notifications."
            }
            // Register the channel with the system; you can't change the importance
            // or other notification behaviors after this
            val notificationManager = getSystemService(
                NotificationManager::class.java
            )
            notificationManager.createNotificationChannel(channel)
        }
    }

    private fun updateFcmPushToken(oldToken: String, newToken: String) {

        object : ServiceConnection {
            override fun onServiceConnected(componentName: ComponentName, service: IBinder) {
                val sinchService = service as SinchService.SinchServiceInterface
                if (sinchService.username != null
                    && sinchService.username.isNotEmpty()
                ) {
                    userId = sinchService.username
                    val userId = userId ?: return
                    try {
                        unregisterPushConfiguration(oldToken, userId)
                        registerPushConfiguration(newToken, userId)
                    } catch (e: IOException) {
                        Log.e(TAG, "Error while updating push token", e)
                    }
                }
            }

            override fun onServiceDisconnected(componentName: ComponentName) {}

            fun update() {
                applicationContext.bindService(
                    Intent(applicationContext, SinchService::class.java), this,
                    BIND_AUTO_CREATE
                )
            }
        }.update()
    }

    private fun unregisterPushConfiguration(token: String, username: String) {
        createUserController(token, username).unregisterPushToken(
            SimplePushTokenUnregistrationCallback()
        )
    }

    private fun registerPushConfiguration(token: String, username: String) {
        createUserController(token, username).registerUser(this, this)
    }

    private fun createUserController(token: String, username: String): UserController {
        return UserController.builder()
            .context(applicationContext)
            .applicationKey(SinchService.APP_KEY)
            .userId(username)
            .environmentHost(SinchService.ENVIRONMENT)
            .pushConfiguration(
                PushConfiguration.fcmPushConfigurationBuilder()
                    .senderID(SinchService.APP_FCM_SENDER_ID)
                    .registrationToken(token)
                    .build()
            )
            .build()
    }

    override fun onPushTokenRegistered() {
        Log.d(TAG, "Successfully registered new push token")
    }

    override fun onPushTokenRegistrationFailed(error: SinchError) {
        Log.e(TAG, "Error while registering new push token: " + error.message)
    }

    override fun onCredentialsRequired(clientRegistration: ClientRegistration) {
        // NB: This implementation just emulates what should be an async procedure, with JWT
        // .create() being
        // run on your backend.
        clientRegistration.register(
            create(
                SinchService.APP_KEY,
                SinchService.APP_SECRET,
                userId.orEmpty()
            )
        )
    }

    override fun onUserRegistered() {
        Log.d(TAG, "Successfully registered user")
    }

    override fun onUserRegistrationFailed(error: SinchError) {
        Log.e(TAG, "Error while registering user: " + error.message)
    }

    companion object {
        private val TAG = FcmListenerService::class.java.simpleName
        var CHANNEL_ID = "Sinch Push Notification Channel"
    }
}