package io.heckel.ntfy.msg

import android.app.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.os.SystemClock
import android.util.Log
import androidx.core.app.NotificationCompat
import io.heckel.ntfy.R
import io.heckel.ntfy.app.Application
import io.heckel.ntfy.data.Subscription
import io.heckel.ntfy.data.topicUrl
import io.heckel.ntfy.ui.MainActivity
import kotlinx.coroutines.*
import okhttp3.Call
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean

/**
 *
 * Largely modeled after this fantastic resource:
 * - https://robertohuertas.com/2019/06/29/android_foreground_services/
 * - https://github.com/robertohuertasm/endless-service/blob/master/app/src/main/java/com/robertohuertas/endless/EndlessService.kt
 */
class SubscriberService : Service() {
    private var wakeLock: PowerManager.WakeLock? = null
    private var isServiceStarted = false
    private val repository by lazy { (application as Application).repository }
    private val jobs = ConcurrentHashMap<Long, Job>() // Subscription ID -> Job
    private val calls = ConcurrentHashMap<Long, Call>() // Subscription ID -> Cal
    private val api = ApiService()
    private val notifier = NotificationService(this)
    private var notificationManager: NotificationManager? = null
    private var notification: Notification? = null

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Log.d(TAG, "onStartCommand executed with startId: $startId")
        if (intent != null) {
            val action = intent.action
            Log.d(TAG, "using an intent with action $action")
            when (action) {
                Actions.START.name -> startService()
                Actions.STOP.name -> stopService()
                else -> Log.e(TAG, "This should never happen. No action in the received intent")
            }
        } else {
            Log.d(TAG, "with a null intent. It has been probably restarted by the system.")
        }
        return START_STICKY // restart if system kills the service
    }

    override fun onCreate() {
        super.onCreate()
        Log.d(TAG, "Subscriber service has been created")

        val title = getString(R.string.channel_subscriber_notification_title)
        val text = getString(R.string.channel_subscriber_notification_text)
        notificationManager = createNotificationChannel()
        notification = createNotification(title, text)

        startForeground(NOTIFICATION_SERVICE_ID, notification)
    }

    override fun onDestroy() {
        super.onDestroy()
        Log.d(TAG, "Subscriber service has been destroyed")
    }

    private fun startService() {
        if (isServiceStarted) {
            launchAndCancelJobs()
            return
        }
        Log.d(TAG, "Starting the foreground service task")
        isServiceStarted = true
        saveServiceState(this, ServiceState.STARTED)
        wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).run {
            newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, WAKE_LOCK_TAG).apply {
                acquire()
            }
        }
        launchAndCancelJobs()
    }

    private fun stopService() {
        Log.d(TAG, "Stopping the foreground service")

        // Cancelling all remaining jobs and open HTTP calls
        jobs.values.forEach { job -> job.cancel() }
        calls.values.forEach { call -> call.cancel() }
        jobs.clear()
        calls.clear()

        // Releasing wake-lock and stopping ourselves
        try {
            wakeLock?.let {
                if (it.isHeld) {
                    it.release()
                }
            }
            stopForeground(true)
            stopSelf()
        } catch (e: Exception) {
            Log.d(TAG, "Service stopped without being started: ${e.message}")
        }

        isServiceStarted = false
        saveServiceState(this, ServiceState.STOPPED)
    }

    private fun launchAndCancelJobs() =
        GlobalScope.launch(Dispatchers.IO) {
            val subscriptions = repository.getSubscriptions().filter { s -> s.instant }
            val subscriptionIds = subscriptions.map { it.id }
            Log.d(TAG, "Refreshing subscriptions")
            Log.d(TAG, "- Subscriptions: $subscriptions")
            Log.d(TAG, "- Jobs: $jobs")
            Log.d(TAG, "- HTTP calls: $calls")
            subscriptions.forEach { subscription ->
                if (!jobs.containsKey(subscription.id)) {
                    jobs[subscription.id] = launchJob(this, subscription)
                }
            }
            jobs.keys().toList().forEach { subscriptionId ->
                if (!subscriptionIds.contains(subscriptionId)) {
                    cancelJob(subscriptionId)
                }
            }
        }

    private fun cancelJob(subscriptionId: Long?) {
        Log.d(TAG, "Cancelling job for $subscriptionId")
        val job = jobs.remove(subscriptionId)
        val call = calls.remove(subscriptionId)
        job?.cancel()
        call?.cancel()
    }

    private fun launchJob(scope: CoroutineScope, subscription: Subscription): Job =
        scope.launch(Dispatchers.IO) {
            val url = topicUrl(subscription.baseUrl, subscription.topic)
            Log.d(TAG, "[$url] Starting connection job")

            // Retry-loop: if the connection fails, we retry unless the job or service is cancelled/stopped
            var since = 0L
            var retryMillis = 0L
            while (isActive && isServiceStarted) {
                Log.d(TAG, "[$url] (Re-)starting subscription for $subscription")
                val startTime = System.currentTimeMillis()
                val notify = { n: io.heckel.ntfy.data.Notification ->
                    since = n.timestamp
                    onNotificationReceived(scope, subscription, n)
                }
                val failed = AtomicBoolean(false)
                val fail = { e: Exception -> failed.set(true) }

                // Call /json subscribe endpoint and loop until the call fails, is canceled,
                // or the job or service are cancelled/stopped
                try {
                    val call = api.subscribe(subscription.id, subscription.baseUrl, subscription.topic, since, notify, fail)
                    calls[subscription.id] = call
                    while (!failed.get() && !call.isCanceled() && isActive && isServiceStarted) {
                        Log.d(TAG, "[$url] Connection is active (failed=$failed, callCanceled=${call.isCanceled()}, jobActive=$isActive, serviceStarted=$isServiceStarted")
                        delay(CONNECTION_LOOP_DELAY_MILLIS) // Resumes immediately if job is cancelled
                    }
                } catch (e: Exception) {
                    Log.e(TAG, "[$url] Connection failed: ${e.message}", e)
                }

                // If we're not cancelled yet, wait little before retrying (incremental back-off)
                if (isActive && isServiceStarted) {
                    retryMillis = nextRetryMillis(retryMillis, startTime)
                    Log.d(TAG, "Connection failed, retrying connection in ${retryMillis/1000}s ...")
                    delay(retryMillis)
                }
            }
            Log.d(TAG, "[$url] Connection job SHUT DOWN")
        }

    private fun onNotificationReceived(scope: CoroutineScope, subscription: Subscription, n: io.heckel.ntfy.data.Notification) {
        val url = topicUrl(subscription.baseUrl, subscription.topic)
        Log.d(TAG, "[$url] Received notification: $n")
        scope.launch(Dispatchers.IO) {
            val added = repository.addNotification(n)
            if (added) {
                Log.d(TAG, "[$url] Showing notification: $n")
                notifier.send(subscription, n.message)
            }
        }
    }

    private fun nextRetryMillis(retryMillis: Long, startTime: Long): Long {
        val connectionDurationMillis = System.currentTimeMillis() - startTime
        if (connectionDurationMillis > RETRY_RESET_AFTER_MILLIS) {
            return RETRY_STEP_MILLIS
        } else if (retryMillis + RETRY_STEP_MILLIS >= RETRY_MAX_MILLIS) {
            return RETRY_MAX_MILLIS
        }
        return retryMillis + RETRY_STEP_MILLIS
    }

    private fun createNotificationChannel(): NotificationManager? {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
            val channelName = getString(R.string.channel_subscriber_service_name) // Show's up in UI
            val channel = NotificationChannel(NOTIFICATION_CHANNEL_ID, channelName, NotificationManager.IMPORTANCE_LOW).let {
                it.setShowBadge(false) // Don't show long-press badge
                it
            }
            notificationManager.createNotificationChannel(channel)
            return notificationManager
        }
        return null
    }

    private fun createNotification(title: String, text: String): Notification {
        val pendingIntent: PendingIntent = Intent(this, MainActivity::class.java).let { notificationIntent ->
            PendingIntent.getActivity(this, 0, notificationIntent, 0)
        }
        return NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
            .setSmallIcon(R.drawable.ic_notification_icon)
            .setContentTitle(title)
            .setContentText(text)
            .setContentIntent(pendingIntent)
            .setSound(null)
            .setShowWhen(false) // Don't show date/time
            .build()
    }

    override fun onBind(intent: Intent): IBinder? {
        return null // We don't provide binding, so return null
    }

    /* This re-schedules the task when the "Clear recent apps" button is pressed */
    override fun onTaskRemoved(rootIntent: Intent) {
        val restartServiceIntent = Intent(applicationContext, SubscriberService::class.java).also {
            it.setPackage(packageName)
        };
        val restartServicePendingIntent: PendingIntent = PendingIntent.getService(this, 1, restartServiceIntent, PendingIntent.FLAG_ONE_SHOT);
        applicationContext.getSystemService(Context.ALARM_SERVICE);
        val alarmService: AlarmManager = applicationContext.getSystemService(Context.ALARM_SERVICE) as AlarmManager;
        alarmService.set(AlarmManager.ELAPSED_REALTIME, SystemClock.elapsedRealtime() + 1000, restartServicePendingIntent);
    }

    /* This re-starts the service on reboot; see manifest */
    class StartReceiver : BroadcastReceiver() {
        override fun onReceive(context: Context, intent: Intent) {
            if (intent.action == Intent.ACTION_BOOT_COMPLETED && readServiceState(context) == ServiceState.STARTED) {
                Intent(context, SubscriberService::class.java).also {
                    it.action = Actions.START.name
                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
                        Log.d(TAG, "Starting subscriber service in >=26 Mode from a BroadcastReceiver")
                        context.startForegroundService(it)
                        return
                    }
                    Log.d(TAG, "Starting subscriber service in < 26 Mode from a BroadcastReceiver")
                    context.startService(it)
                }
            }
        }
    }

    enum class Actions {
        START,
        STOP
    }

    enum class ServiceState {
        STARTED,
        STOPPED,
    }

    companion object {
        private const val TAG = "NtfySubscriberService"
        private const val WAKE_LOCK_TAG = "SubscriberService:lock"
        private const val NOTIFICATION_CHANNEL_ID = "ntfy-subscriber"
        private const val NOTIFICATION_SERVICE_ID = 2586
        private const val SHARED_PREFS_ID = "SubscriberService"
        private const val SHARED_PREFS_SERVICE_STATE = "ServiceState"
        private const val CONNECTION_LOOP_DELAY_MILLIS = 30_000L
        private const val RETRY_STEP_MILLIS = 5_000L
        private const val RETRY_MAX_MILLIS = 60_000L
        private const val RETRY_RESET_AFTER_MILLIS = 60_000L // Must be larger than CONNECTION_LOOP_DELAY_MILLIS

        fun saveServiceState(context: Context, state: ServiceState) {
            val sharedPrefs = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
            sharedPrefs.edit()
                .putString(SHARED_PREFS_SERVICE_STATE, state.name)
                .apply()
        }

        fun readServiceState(context: Context): ServiceState {
            val sharedPrefs = context.getSharedPreferences(SHARED_PREFS_ID, Context.MODE_PRIVATE)
            val value = sharedPrefs.getString(SHARED_PREFS_SERVICE_STATE, ServiceState.STOPPED.name)
            return ServiceState.valueOf(value!!)
        }
    }
}