package com.soulstream.transfernotice

import android.app.Notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.ServiceInfo
import android.os.Build
import android.os.IBinder
import androidx.core.app.NotificationCompat
import androidx.core.app.ServiceCompat
import androidx.core.content.ContextCompat

/**
 * What is being fetched, shared between the plugin (which hears the
 * WebView) and the service (which owns the notice). The service is started
 * on the first update and refreshed on every one after; a count of zero is
 * a clear, since the phone's own rule is that an empty queue has no notice.
 */
object TransferHub {
    const val CHANNEL = "downloads"

    /** 1 is the media card's, and ids are per-package */
    const val NOTIFICATION_ID = 2

    const val ACTION_CANCEL = "com.soulstream.transfernotice.CANCEL"
    const val ACTION_OPEN = "com.soulstream.transfernotice.OPEN"

    @Volatile var onAction: ((String) -> Unit)? = null
    @Volatile var state: NoticeArgs? = null
    @Volatile var service: TransferService? = null

    /** Its own channel, low importance and no badge: a download is not an
     *  alarm, and silencing it must not silence the player's card. A
     *  filename is not for a locked screen, so the notice is private there. */
    fun ensureChannel(ctx: Context) {
        if (Build.VERSION.SDK_INT < 26) return
        val manager = ctx.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
        if (manager.getNotificationChannel(CHANNEL) != null) return
        val channel = NotificationChannel(CHANNEL, "Downloads", NotificationManager.IMPORTANCE_LOW)
        channel.description = "What Save is fetching into your library"
        channel.setShowBadge(false)
        channel.lockscreenVisibility = Notification.VISIBILITY_PRIVATE
        manager.createNotificationChannel(channel)
    }

    fun update(ctx: Context, np: NoticeArgs) {
        if (np.count <= 0) {
            clear(ctx)
            return
        }
        state = np
        val running = service
        if (running == null) {
            /* Android 12+ refuses a foreground start from the background. A
               Save is always begun from the foreground, so the first start
               lands; one that cannot simply leaves the fetch without a card */
            try {
                ContextCompat.startForegroundService(ctx, Intent(ctx, TransferService::class.java))
            } catch (e: Exception) {
                android.util.Log.w("SoulStream", "transfer notice could not start: $e")
            }
        } else {
            running.refresh()
        }
    }

    fun clear(ctx: Context) {
        state = null
        val running = service
        /* refresh() on a null state is the one path that takes the notice
           down and stops the service; stopService is for a service that was
           never here */
        if (running != null) running.refresh() else ctx.stopService(Intent(ctx, TransferService::class.java))
    }

    fun act(action: String) {
        onAction?.invoke(action)
    }
}

/**
 * The foreground service behind the notice: a progress notification with
 * Stop all and Open on it, and — the point — the reason the process is not
 * frozen while a Save runs with nothing playing. The fetches themselves
 * stay where they are, on the Rust core's own connections; this only says
 * what they are doing.
 */
class TransferService : Service() {
    private var inForeground = false

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

    override fun onCreate() {
        super.onCreate()
        TransferHub.ensureChannel(this)
        TransferHub.service = this
    }

    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        /* Started with startForegroundService, so Android demands
           startForeground() promptly WHATEVER the state — a queue that
           emptied while the start was in flight included — or it kills the
           whole app about ten seconds later. MediaService learned this the
           same way, on 2026-09-13, and it was a Save that died. Promote
           first, with the notice there is or a plain one; refresh() steps
           back at once if there is nothing left to say. */
        if (!inForeground) promote(build(TransferHub.state ?: NoticeArgs()))
        when (intent?.action) {
            TransferHub.ACTION_CANCEL -> TransferHub.act("cancel")
            TransferHub.ACTION_OPEN -> {
                packageManager.getLaunchIntentForPackage(packageName)?.let {
                    it.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    try { startActivity(it) } catch (_: Exception) { /* no launcher activity */ }
                }
                TransferHub.act("open")
            }
        }
        refresh()
        return START_NOT_STICKY
    }

    /** Mirror TransferHub.state into the notice — or take it down. */
    fun refresh() {
        val np = TransferHub.state
        if (np == null) {
            /* the queue emptied: the notice goes, whole */
            if (inForeground) {
                ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_REMOVE)
                inForeground = false
            }
            stopSelf()
            return
        }
        /* promote() on an already-promoted service is just a notify */
        promote(build(np))
    }

    private fun promote(notification: Notification) {
        if (Build.VERSION.SDK_INT >= 29) {
            ServiceCompat.startForeground(
                this, TransferHub.NOTIFICATION_ID, notification,
                ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC,
            )
        } else {
            startForeground(TransferHub.NOTIFICATION_ID, notification)
        }
        inForeground = true
    }

    /** Android 15 caps a dataSync service at six hours a day and kills the
     *  app if it is still up when this returns. Take the notice down: the
     *  fetches are the Rust core's own and Transfers still lists them. */
    override fun onTimeout(startId: Int, fgsType: Int) {
        TransferHub.clear(applicationContext)
    }

    override fun onDestroy() {
        TransferHub.service = null
        super.onDestroy()
    }

    private fun build(np: NoticeArgs): Notification {
        val open = serviceIntent(11, TransferHub.ACTION_OPEN)
        val cancel = serviceIntent(12, TransferHub.ACTION_CANCEL)
        return NotificationCompat.Builder(this, TransferHub.CHANNEL)
            .setSmallIcon(R.drawable.ic_soulstream_down)
            .setContentTitle(np.title ?: "Saving")
            .setContentText(np.line ?: "")
            .setProgress(100, np.percent.coerceIn(0, 100), np.indeterminate)
            .setContentIntent(open)
            .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
            .setCategory(NotificationCompat.CATEGORY_PROGRESS)
            .setOnlyAlertOnce(true)
            .setShowWhen(false)
            .setOngoing(true)
            .addAction(android.R.drawable.ic_menu_close_clear_cancel, "Stop all", cancel)
            .addAction(android.R.drawable.ic_menu_upload, "Open", open)
            .build()
    }

    /** The notice only exists while this service is in the foreground, so
     *  its own buttons are never a background service start. The request
     *  codes are 11 and 12 on purpose — the media plugin uses 0 in the same
     *  package, and two PendingIntents that filterEqual overwrite each other. */
    private fun serviceIntent(code: Int, action: String): PendingIntent {
        val i = Intent(this, TransferService::class.java).setAction(action)
        return PendingIntent.getForegroundService(
            this, code, i,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )
    }
}
