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

import android.Manifest
import android.annotation.TargetApi
import android.app.*
import android.content.Context
import android.content.Intent
import android.content.SharedPreferences
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.graphics.Canvas
import android.os.*
import android.util.Log
import androidx.core.app.NotificationCompat
import com.google.firebase.FirebaseApp
import com.sinch.android.rtc.*
import com.sinch.android.rtc.calling.*
import com.sinch.android.rtc.sample.video.push.FcmTokenPersistence.getFcmRegistrationToken
import com.sinch.android.rtc.sample.video.push.JWT.create
import com.sinch.android.rtc.sample.video.push.fcm.FcmListenerService.Companion.CALL_NOTIFICATION_ID
import com.sinch.android.rtc.sample.video.push.fcm.FcmListenerService.Companion.CALL_PUSH_CHANNEL_ID
import com.sinch.android.rtc.video.VideoController
import java.io.IOException

class SinchService : Service() {

    companion object {
        private val TAG = SinchService::class.java.simpleName
        val APP_FCM_SENDER_ID: String = FirebaseApp.getInstance().options.gcmSenderId.orEmpty()

        const val APP_KEY = "enter-application-key"
        const val APP_SECRET = "enter-application-secret"
        const val ENVIRONMENT = "ocra.api.sinch.com"

        const val MESSAGE_PERMISSIONS_NEEDED = 1
        const val REQUIRED_PERMISSION = "REQUIRED_PERMISSION"
        const val MESSENGER = "MESSENGER"
        const val CALL_ID = "CALL_ID"
    }

    private val settings: PersistedSettings by lazy {
        PersistedSettings(applicationContext)
    }
    private val sinchServiceInterface: SinchServiceInterface = SinchServiceInterface()

    private var messenger: Messenger? = null
    private var sinchClient: SinchClient? = null
    private var onSinchClientStarted: ()-> Unit = { }
    private var onSinchClientFailed: (SinchError) -> Unit = {  }

    override fun onCreate() {
        super.onCreate()
        attemptAutoStart()
    }

    private fun attemptAutoStart() {
        if (hasUsername()) {
            start()
        }
    }

    private fun createClient(username: String) {
        sinchClient = SinchClient.builder().context(applicationContext)
            .userId(username)
            .applicationKey(APP_KEY)
            .environmentHost(ENVIRONMENT)
            .pushConfiguration(
                PushConfiguration.fcmPushConfigurationBuilder()
                    .senderID(APP_FCM_SENDER_ID)
                    .registrationToken(getFcmRegistrationToken(this).orEmpty()).build()
            )
            .pushNotificationDisplayName("User $username")
            .build()
        sinchClient?.addSinchClientListener(MySinchClientListener())
        sinchClient?.callController?.addCallControllerListener(SinchCallControllerListener())
    }

    override fun onDestroy() {
        if (sinchClient?.isStarted == true) {
            sinchClient?.terminateGracefully()
        }
        super.onDestroy()
    }

    private fun hasUsername(): Boolean {
        if (settings.username.isEmpty()) {
            return false
        }
        return true
    }

    private fun createClientIfNecessary() {
        if (sinchClient != null) {
            return
        }
        check(hasUsername()) { "Can't create a SinchClient as no username is available!" }
        createClient(settings.username)
    }

    private fun start() {
        var permissionsGranted = true
        try {
            createClientIfNecessary()
            //mandatory checks
            sinchClient?.checkManifest()
            // Extra permission needed for video calling
            if (applicationContext.checkCallingOrSelfPermission(
                    Manifest.permission.CAMERA
                ) != PackageManager.PERMISSION_GRANTED
            ) {
                permissionsGranted = false
                handleMissingPermission(Manifest.permission.CAMERA)
            }
        } catch (e: MissingPermissionException) {
            permissionsGranted = false
            handleMissingPermission(e.requiredPermission)
        } catch (e: IOException) {
            Log.e(TAG, "Failed to create SinchClient ", e)
            return
        }
        if (permissionsGranted) {
            Log.d(TAG, "Starting SinchClient")
            try {
                sinchClient?.start()
            } catch (e: IllegalStateException) {
                Log.w(TAG, "Can't start SinchClient - " + e.message)
            }
        }
    }

    private fun handleMissingPermission(permission: String) {
        val message = Message.obtain().apply {
            data = Bundle().apply { putString(REQUIRED_PERMISSION, permission) }
            what = MESSAGE_PERMISSIONS_NEEDED
        }
        messenger?.send(message)
    }

    private fun stop() {
        sinchClient?.terminateGracefully()
        sinchClient = null
    }

    override fun onBind(intent: Intent): IBinder {
        messenger = intent.getParcelableExtra(MESSENGER)
        return sinchServiceInterface
    }

    inner class SinchServiceInterface : Binder() {

        val videoController: VideoController?
            get() = sinchClient?.videoController

        val audioController: AudioController?
            get() = sinchClient?.audioController


        val isStarted: Boolean
            get() = sinchClient?.isStarted == true

        var username: String
            get() = settings.username
            set(value) {
                settings.username = value
            }

        fun callUserVideo(userId: String): Call {
            val sinchClient = sinchClient ?: throw RuntimeException("Sinch Client is not created")
            return sinchClient.callController.callUser(userId, MediaConstraints(true))
        }

        fun retryStartAfterPermissionGranted() {
            attemptAutoStart()
        }

        fun startClient() {
            start()
        }

        fun stopClient() {
            stop()
        }

        fun unregisterPushToken() {
            settings.username = ""
            sinchClient?.unregisterPushToken(SimplePushTokenUnregistrationCallback())
        }

        fun setOnSinchClientStartedCallback(callback: ()-> Unit) {
            this@SinchService.onSinchClientStarted = callback
        }

        fun removeOnSinchClientStartedCallback() {
            this@SinchService.onSinchClientStarted = {}
        }

        fun setOnSinchClientFailedCallback(callback: (SinchError)-> Unit) {
            this@SinchService.onSinchClientFailed = callback
        }

        fun removeOnSinchClientFailedCallback() {
            this@SinchService.onSinchClientFailed = {}
        }

        fun getCall(callId: String): Call? {
            return sinchClient?.callController?.getCall(callId)
        }

        fun relayRemotePushNotificationPayload(result: CallNotificationResult) {
            if (!hasUsername()) {
                Log.e(TAG, "Unable to relay the push notification!")
                return
            }
            createClientIfNecessary()
            sinchClient?.relayRemotePushNotification(result)
        }
    }

    private inner class MySinchClientListener : SinchClientListener {

        override fun onClientFailed(client: SinchClient, error: SinchError) {
            onSinchClientFailed(error)
            sinchClient?.terminateGracefully()
            sinchClient = null
        }

        override fun onClientStarted(client: SinchClient) {
            Log.d(TAG, "SinchClient started")
            onSinchClientStarted()
        }

        override fun onLogMessage(level: Int, area: String, message: String, throwable: Throwable?) {
            when (level) {
                Log.DEBUG -> Log.d(area, message, throwable)
                Log.ERROR -> Log.e(area, message, throwable)
                Log.INFO -> Log.i(area, message, throwable)
                Log.VERBOSE -> Log.v(area, message, throwable)
                Log.WARN -> Log.w(area, message, throwable)
                else -> {}
            }
        }

        override fun onCredentialsRequired(clientRegistration: ClientRegistration) {
            // Normally, should not be asked again, since the user / push token is registered via
            // UserController,
            // but you have to provide valid registration here anyway, in case previous registration
            // is somehow compromised.
            clientRegistration.register(create(APP_KEY, APP_SECRET, settings.username))
        }

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

        override fun onUserRegistrationFailed(error: SinchError) {
            Log.e(TAG, "User registration failed: " + error.message)
        }

        override fun onPushTokenRegistered() {
            Log.d(TAG, "Push token registered.")
        }

        override fun onPushTokenRegistrationFailed(sinchError: SinchError) {
            Log.e(TAG, "Push token registration failed." + sinchError.message)
        }

        override fun onPushTokenUnregistered() {
            Log.d(TAG, "Push token unregistered.")
        }

        override fun onPushTokenUnregistrationFailed(sinchError: SinchError) {
            Log.e(TAG, "Push token unregistration failed." + sinchError.message)
        }
    }

    private inner class SinchCallControllerListener : CallControllerListener, CallListener {

        override fun onIncomingCall(callController: CallController, call: Call) {
            Log.d(TAG, "onIncomingCall: " + call.callId)

            val intent = Intent(applicationContext, IncomingCallScreenActivity::class.java)
                .apply {
                    putExtra(CALL_ID, call.callId)
                }
            val inForeground = isAppOnForeground(applicationContext)

            if (!inForeground) {
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
            } else {
                intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            }

            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q && !inForeground) {
                (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).notify(
                    CALL_NOTIFICATION_ID,
                    createIncomingCallNotification(call.remoteUserId, intent)
                )
            } else {
                applicationContext.startActivity(intent)
            }
        }

        private fun isAppOnForeground(context: Context): Boolean {
            val activityManager = context.getSystemService(ACTIVITY_SERVICE) as ActivityManager
            val appProcesses = activityManager.runningAppProcesses ?: return false
            val packageName = context.packageName
            for (appProcess in appProcesses) {
                if (appProcess.importance
                    == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
                    && appProcess.processName == packageName
                ) {
                    return true
                }
            }
            return false
        }

        private fun getBitmap(context: Context, resId: Int): Bitmap {
            val largeIconWidth = context.resources
                .getDimension(android.R.dimen.notification_large_icon_width).toInt()
            val largeIconHeight = context.resources
                .getDimension(android.R.dimen.notification_large_icon_height).toInt()
            val drawable = context.resources.getDrawable(resId)
            val bitmap =
                Bitmap.createBitmap(largeIconWidth, largeIconHeight, Bitmap.Config.ARGB_8888)
            val canvas = Canvas(bitmap)
            drawable.setBounds(0, 0, largeIconWidth, largeIconHeight)
            drawable.draw(canvas)
            return bitmap
        }

        private fun getPendingIntent(intent: Intent, action: String): PendingIntent {
            intent.action = action
            return PendingIntent.getActivity(
                applicationContext, 111, intent,
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            )
        }

        @TargetApi(29)
        private fun createIncomingCallNotification(
            userId: String,
            fullScreenIntent: Intent
        ): Notification {
            val pendingIntent = PendingIntent.getActivity(
                applicationContext, 112, fullScreenIntent,
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
            )
            val builder = NotificationCompat.Builder(
                applicationContext,
                CALL_PUSH_CHANNEL_ID
            )
                .setContentTitle("Incoming call")
                .setContentText(userId)
                .setLargeIcon(getBitmap(applicationContext, R.drawable.icon))
                .setSmallIcon(R.drawable.ic_outline_local_phone_24)
                .setPriority(NotificationCompat.PRIORITY_MAX)
                .setContentIntent(pendingIntent)
                .setFullScreenIntent(pendingIntent, true)
                .addAction(
                    R.drawable.button_accept, "Answer",
                    getPendingIntent(fullScreenIntent, IncomingCallScreenActivity.ACTION_ANSWER)
                )
                .addAction(
                    R.drawable.button_decline, "Ignore",
                    getPendingIntent(fullScreenIntent, IncomingCallScreenActivity.ACTION_IGNORE)
                )
                .setAutoCancel(true)
                .setOngoing(true)
            return builder.build()
        }

        override fun onCallEstablished(call: Call) {
            hideCallNotificationIfNeeded()
        }

        override fun onCallProgressing(call: Call) {}

        override fun onCallEnded(call: Call) {
            hideCallNotificationIfNeeded()
            call.removeCallListener(this)
        }

        private fun hideCallNotificationIfNeeded() {
            (getSystemService(NOTIFICATION_SERVICE) as NotificationManager).cancel(CALL_NOTIFICATION_ID)
        }

    }

    private inner class PersistedSettings(context: Context) {
        private val prefKey = "Sinch"
        private val store: SharedPreferences = context.getSharedPreferences(prefKey, MODE_PRIVATE)

        var username: String
            get() = store.getString("Username", "").orEmpty()
            set(username) {
                store.edit()
                    .putString("Username", username)
                    .commit()
            }
    }

}
