package ru.forbion.f7cloud.core.push

import android.app.AlarmManager
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Handler
import android.os.Looper
import android.util.Log
import androidx.core.app.NotificationCompat
import androidx.core.app.Person
import org.json.JSONArray
import org.json.JSONObject
import ru.forbion.f7cloud.core.auth.AuthStore

/**
 * Shows one incoming Talk call at a time; further calls wait in a FIFO queue.
 */
object F7IncomingCallQueue {
    private const val TAG = "F7IncomingCallQueue"
    private const val PREFS = "f7push_call_queue"
    private const val KEY_QUEUE = "queue"
    private const val KEY_ACTIVE_TOKEN = "active_token"
    private const val KEY_ACTIVE_AT = "active_at"
    private const val KEY_ACTIVE_CALL = "active_call"
    const val ACTIVE_NOTIFICATION_ID = 5000
    private const val MISSED_NOTIFICATION_BASE = 5100
    private const val ACTIVE_RING_TTL_MS = 3 * 60 * 1000L

    /** Входящий звонит 30 с; дальше — авто-сброс и уведомление «Пропущенный звонок». */
    private const val RING_TIMEOUT_MS = 30_000L

    /** Запас страховочного alarm поверх основного in-process таймера. */
    private const val TIMEOUT_ALARM_SLACK_MS = 5_000L

    private val lock = Any()
    private val handler = Handler(Looper.getMainLooper())
    private var timeoutRunnable: Runnable? = null
    private val callEndedListeners = java.util.concurrent.CopyOnWriteArraySet<(String) -> Unit>()

    /** Уведомляет UI (полноэкранный входящий), что активный звонок снят: принят/отклонён/протух. */
    fun addCallEndedListener(listener: (String) -> Unit) {
        callEndedListeners.add(listener)
    }

    fun removeCallEndedListener(listener: (String) -> Unit) {
        callEndedListeners.remove(listener)
    }

    private fun notifyCallEnded(token: String) {
        callEndedListeners.forEach { listener ->
            runCatching { listener(token) }
        }
    }

    fun enqueue(
        context: Context,
        title: String,
        body: String,
        acceptUrl: String?,
        roomToken: String?,
        roomDisplayName: String? = null,
    ): Boolean {
        val token = normalizeToken(roomToken, acceptUrl, title)
        val displayName = TalkCallPushLabels.resolveRoomDisplayName(title, body, roomDisplayName)
        synchronized(lock) {
            val prefs = prefs(context)
            expireStaleActiveLocked(prefs)

            val call = PendingCall(token, title, body, acceptUrl, displayName)
            val active = prefs.getString(KEY_ACTIVE_TOKEN, "").orEmpty()

            if (token == active) {
                // Duplicate FCM for the same room — refresh UI only, do not re-ring.
                val shown = showNotification(context, call, waiting = 0, alert = false)
                if (shown) {
                    touchActive(prefs)
                }
                return shown
            }

            val queue = readQueue(prefs)
            if (containsToken(queue, token)) {
                return false
            }

            if (active.isEmpty()) {
                val shown = showNotification(context, call, waiting = 0)
                if (shown) {
                    setActive(prefs, call)
                    scheduleTimeout(context, token)
                }
                return shown
            }

            queue.put(call.toJson())
            prefs.edit().putString(KEY_QUEUE, queue.toString()).apply()
            Log.d(TAG, "Call queued: $token, queue size=${queue.length()}")
            return false
        }
    }

    fun dismissAndShowNext(context: Context, roomToken: String?) {
        var ended: String? = null
        synchronized(lock) {
            val prefs = prefs(context)
            val active = prefs.getString(KEY_ACTIVE_TOKEN, "").orEmpty()
            var queue = readQueue(prefs)

            if (!roomToken.isNullOrBlank()) {
                val token = roomToken.trim()
                if (token != active) {
                    queue = removeToken(queue, token)
                    prefs.edit().putString(KEY_QUEUE, queue.toString()).apply()
                    return
                }
            }

            cancelTimeout(context)
            cancelNotification(context)
            clearActive(prefs)
            ended = active.takeIf { it.isNotEmpty() }
            advanceQueueLocked(context, prefs, queue)
        }
        ended?.let { notifyCallEnded(it) }
    }

    /**
     * Авто-сброс не отвеченного звонка (истекли [RING_TIMEOUT_MS]): снять входящий,
     * показать «Пропущенный звонок» и следующий звонок из очереди, если есть.
     * Принятые/отклонённые звонки сюда не попадают — их снимает [dismissAndShowNext].
     */
    fun timeoutActive(context: Context, roomToken: String?) {
        var ended: String? = null
        synchronized(lock) {
            val prefs = prefs(context)
            val active = prefs.getString(KEY_ACTIVE_TOKEN, "").orEmpty()
            if (active.isEmpty()) return
            if (!roomToken.isNullOrBlank() && roomToken.trim() != active) return

            val call = readActiveCall(prefs)
            cancelTimeout(context)
            cancelNotification(context)
            clearActive(prefs)
            ended = active
            Log.i(TAG, "Call timed out (unanswered): $active")
            if (call != null) {
                showMissedNotification(context, call)
            }
            advanceQueueLocked(context, prefs, readQueue(prefs))
        }
        ended?.let { notifyCallEnded(it) }
    }

    fun clearAll(context: Context) {
        synchronized(lock) {
            val prefs = prefs(context)
            cancelTimeout(context)
            cancelNotification(context)
            prefs.edit().remove(KEY_QUEUE).remove(KEY_ACTIVE_TOKEN).remove(KEY_ACTIVE_AT).remove(KEY_ACTIVE_CALL).apply()
        }
    }

    private fun advanceQueueLocked(
        context: Context,
        prefs: android.content.SharedPreferences,
        queue: JSONArray,
    ) {
        if (queue.length() == 0) {
            prefs.edit().remove(KEY_QUEUE).apply()
            return
        }
        runCatching {
            val next = PendingCall.fromJson(queue.getJSONObject(0))
            val rest = JSONArray()
            for (i in 1 until queue.length()) {
                rest.put(queue.get(i))
            }
            prefs.edit().putString(KEY_QUEUE, rest.toString()).apply()
            if (showNotification(context, next, rest.length())) {
                setActive(prefs, next)
                scheduleTimeout(context, next.roomToken)
            }
        }.onFailure {
            Log.w(TAG, "Failed to parse queued call", it)
            prefs.edit().remove(KEY_QUEUE).apply()
        }
    }

    private fun showNotification(
        context: Context,
        call: PendingCall,
        waiting: Int,
        alert: Boolean = true,
    ): Boolean {
        F7NotificationChannels.ensureAll(context)
        val body = if (waiting > 0) {
            call.body + "\n" + context.resources.getQuantityString(
                R.plurals.call_queue_waiting,
                waiting,
                waiting,
            )
        } else {
            call.body
        }

        val joinUrl = resolveJoinUrl(context, call) ?: run {
            Log.w(TAG, "Cannot resolve join URL for call ${call.roomToken}")
            return false
        }

        val intents = buildPendingIntents(context, call, joinUrl)
        if (alert) {
            runCatching { F7IncomingCallRinger.start(context, call.roomToken) }
                .onFailure { Log.w(TAG, "Ringtone start failed", it) }
        }

        val posted = runCatching {
            postCallStyleNotification(context, call, body, intents, alert)
        }.onFailure {
            Log.w(TAG, "CallStyle notification failed, using fallback", it)
        }.isSuccess || runCatching {
            postFallbackNotification(context, call, body, intents, alert)
        }.onFailure {
            Log.e(TAG, "Fallback call notification failed", it)
        }.isSuccess

        if (!posted) {
            F7IncomingCallRinger.stop()
        }
        return posted
    }

    private data class CallPendingIntents(
        val accept: PendingIntent,
        val preview: PendingIntent,
        val decline: PendingIntent,
        val fullScreen: PendingIntent,
    )

    private fun buildPendingIntents(
        context: Context,
        call: PendingCall,
        joinUrl: String,
    ): CallPendingIntents {
        val requestCode = ACTIVE_NOTIFICATION_ID + kotlin.math.abs(call.roomToken.hashCode() % 10000)
        val accept = PendingIntent.getActivity(
            context,
            requestCode,
            incomingCallIntent(context, call, joinUrl, autoAccept = true),
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )
        val preview = PendingIntent.getActivity(
            context,
            requestCode + 50000,
            incomingCallIntent(context, call, joinUrl, autoAccept = false),
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )
        val declineIntent = Intent(context, F7CallActionReceiver::class.java).apply {
            action = F7CallActionReceiver.ACTION_DECLINE
            putExtra(F7CallActionReceiver.EXTRA_NOTIFICATION_ID, ACTIVE_NOTIFICATION_ID)
            putExtra(F7CallActionReceiver.EXTRA_ROOM_TOKEN, call.roomToken)
        }
        val decline = PendingIntent.getBroadcast(
            context,
            ACTIVE_NOTIFICATION_ID + 1,
            declineIntent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )
        val fullScreen = PendingIntent.getActivity(
            context,
            requestCode + 60000,
            incomingCallIntent(context, call, joinUrl, autoAccept = false),
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )
        return CallPendingIntents(accept, preview, decline, fullScreen)
    }

    private fun postCallStyleNotification(
        context: Context,
        call: PendingCall,
        body: String,
        intents: CallPendingIntents,
        alert: Boolean,
    ) {
        val caller = Person.Builder()
            .setName(call.displayName.ifBlank { call.title.ifBlank { context.getString(R.string.incoming_call_subtitle) } })
            .build()

        val canFullScreen = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            context.getSystemService(NotificationManager::class.java)
                ?.canUseFullScreenIntent() != false
        } else {
            true
        }

        val callStyle = NotificationCompat.CallStyle.forIncomingCall(
            caller,
            intents.decline,
            intents.accept,
        )

        val notification = NotificationCompat.Builder(context, F7NotificationChannels.CALLS)
            .setSmallIcon(android.R.drawable.stat_sys_phone_call)
            .setContentTitle(call.displayName.ifBlank { call.title })
            .setContentText(body)
            .setStyle(callStyle)
            .setCategory(NotificationCompat.CATEGORY_CALL)
            .setPriority(NotificationCompat.PRIORITY_MAX)
            .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
            .setPublicVersion(genericCallPublic(context, intents.preview))
            .setOngoing(true)
            .setAutoCancel(false)
            .setOnlyAlertOnce(true)
            .setSound(null)
            .setDefaults(0)
            .setVibrate(null)
            .setContentIntent(intents.preview)
            .apply {
                if (canFullScreen) {
                    setFullScreenIntent(intents.fullScreen, true)
                }
            }
            .build()

        context.getSystemService(NotificationManager::class.java)
            ?.notify(ACTIVE_NOTIFICATION_ID, notification)
            ?: throw IllegalStateException("NotificationManager unavailable")
    }

    private fun postFallbackNotification(
        context: Context,
        call: PendingCall,
        body: String,
        intents: CallPendingIntents,
        alert: Boolean,
    ) {
        val canFullScreen = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
            context.getSystemService(NotificationManager::class.java)
                ?.canUseFullScreenIntent() != false
        } else {
            true
        }

        val notification = NotificationCompat.Builder(context, F7NotificationChannels.CALLS)
            .setSmallIcon(android.R.drawable.stat_sys_phone_call)
            .setContentTitle(call.displayName.ifBlank { call.title })
            .setContentText(body)
            .setStyle(NotificationCompat.BigTextStyle().bigText(body))
            .setCategory(NotificationCompat.CATEGORY_CALL)
            .setPriority(NotificationCompat.PRIORITY_MAX)
            .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
            .setPublicVersion(genericCallPublic(context, intents.preview))
            .setOngoing(true)
            .setAutoCancel(false)
            .setOnlyAlertOnce(true)
            .setSound(null)
            .setDefaults(0)
            .setVibrate(null)
            .setContentIntent(intents.preview)
            .apply {
                if (canFullScreen) {
                    setFullScreenIntent(intents.fullScreen, true)
                }
            }
            .addAction(0, context.getString(R.string.call_action_accept), intents.accept)
            .addAction(0, context.getString(R.string.call_action_decline), intents.decline)
            .build()

        context.getSystemService(NotificationManager::class.java)
            ?.notify(ACTIVE_NOTIFICATION_ID, notification)
            ?: throw IllegalStateException("NotificationManager unavailable")
    }

    /**
     * Обезличенная (public) версия уведомления звонка для локскрина: показывает лишь «Входящий
     * звонок» без имени звонящего/комнаты (минимум информации, решение владельца). Полное имя —
     * в полноэкранном UI звонка/после разблокировки. Full-screen intent работает независимо от visibility.
     */
    private fun genericCallPublic(context: Context, preview: PendingIntent): android.app.Notification =
        NotificationCompat.Builder(context, F7NotificationChannels.CALLS)
            .setSmallIcon(android.R.drawable.stat_sys_phone_call)
            .setContentTitle("F7cloud")
            .setContentText("Входящий звонок")
            .setCategory(NotificationCompat.CATEGORY_CALL)
            .setPriority(NotificationCompat.PRIORITY_MAX)
            .setContentIntent(preview)
            .build()

    private fun resolveJoinUrl(context: Context, call: PendingCall): String? {
        val raw = call.acceptUrl?.takeIf { it.isNotBlank() }
            ?: AuthStore(context).load()?.let { session ->
                buildCallUrl(session.serverUrl, call.roomToken)
            }
            ?: return null
        return stripDirectCallHash(raw)
    }

    private fun buildCallUrl(serverBase: String, roomToken: String): String {
        val base = serverBase.trimEnd('/')
        return "$base/call/${roomToken.trim()}"
    }

    private fun stripDirectCallHash(url: String): String {
        val hash = url.indexOf('#')
        return if (hash >= 0) url.substring(0, hash) else url
    }

    private fun prefs(context: Context) =
        context.applicationContext.getSharedPreferences(PREFS, Context.MODE_PRIVATE)

    private fun setActive(prefs: android.content.SharedPreferences, call: PendingCall) {
        prefs.edit()
            .putString(KEY_ACTIVE_TOKEN, call.roomToken)
            .putLong(KEY_ACTIVE_AT, System.currentTimeMillis())
            // Персистим весь звонок: страховочный alarm после смерти процесса должен уметь
            // показать «Пропущенный» с названием комнаты.
            .putString(KEY_ACTIVE_CALL, call.toJson().toString())
            .apply()
    }

    private fun clearActive(prefs: android.content.SharedPreferences) {
        prefs.edit().remove(KEY_ACTIVE_TOKEN).remove(KEY_ACTIVE_AT).remove(KEY_ACTIVE_CALL).apply()
    }

    private fun readActiveCall(prefs: android.content.SharedPreferences): PendingCall? {
        val raw = prefs.getString(KEY_ACTIVE_CALL, null) ?: return null
        return runCatching { PendingCall.fromJson(JSONObject(raw)) }.getOrNull()
    }

    private fun scheduleTimeout(context: Context, token: String) {
        val app = context.applicationContext
        timeoutRunnable?.let(handler::removeCallbacks)
        val runnable = Runnable { timeoutActive(app, token) }
        timeoutRunnable = runnable
        handler.postDelayed(runnable, RING_TIMEOUT_MS)
        // Страховка: если процесс умрёт до срабатывания таймера, alarm поднимет его и
        // заменит зависшее уведомление звонка на «Пропущенный». timeoutActive идемпотентен.
        runCatching {
            app.getSystemService(AlarmManager::class.java)?.setAndAllowWhileIdle(
                AlarmManager.RTC_WAKEUP,
                System.currentTimeMillis() + RING_TIMEOUT_MS + TIMEOUT_ALARM_SLACK_MS,
                timeoutAlarmIntent(app, token),
            )
        }.onFailure { Log.w(TAG, "Timeout alarm schedule failed", it) }
    }

    private fun cancelTimeout(context: Context) {
        timeoutRunnable?.let(handler::removeCallbacks)
        timeoutRunnable = null
        runCatching {
            context.applicationContext.getSystemService(AlarmManager::class.java)
                ?.cancel(timeoutAlarmIntent(context.applicationContext, null))
        }
    }

    private fun timeoutAlarmIntent(context: Context, token: String?): PendingIntent {
        val intent = Intent(context, F7CallActionReceiver::class.java).apply {
            action = F7CallActionReceiver.ACTION_CALL_TIMEOUT
            if (token != null) {
                putExtra(F7CallActionReceiver.EXTRA_ROOM_TOKEN, token)
            }
        }
        return PendingIntent.getBroadcast(
            context,
            ACTIVE_NOTIFICATION_ID + 2,
            intent,
            PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
        )
    }

    private fun showMissedNotification(context: Context, call: PendingCall) {
        F7NotificationChannels.ensureAll(context)
        val manager = context.getSystemService(NotificationManager::class.java) ?: return
        val roomName = call.displayName.ifBlank { call.title }
        val notificationId = MISSED_NOTIFICATION_BASE + kotlin.math.abs(call.roomToken.hashCode() % 1000)
        val pending = context.packageManager.getLaunchIntentForPackage(context.packageName)?.let { launch ->
            PendingIntent.getActivity(
                context,
                notificationId,
                Intent(launch).putExtra(PushIntents.EXTRA_ROOM_TOKEN, call.roomToken),
                PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
            )
        }
        val iconRes = context.applicationInfo.icon.takeIf { it != 0 }
            ?: android.R.drawable.sym_call_missed
        // Локскрин — обезличенно (минимум информации, решение владельца); название комнаты
        // видно после разблокировки.
        val publicVersion = NotificationCompat.Builder(context, F7NotificationChannels.MESSAGES)
            .setSmallIcon(iconRes)
            .setContentTitle("F7cloud")
            .setContentText(context.getString(R.string.call_missed_public))
            .setAutoCancel(true)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .apply { if (pending != null) setContentIntent(pending) }
            .build()
        val notification = NotificationCompat.Builder(context, F7NotificationChannels.MESSAGES)
            .setSmallIcon(iconRes)
            .setContentTitle(context.getString(R.string.call_missed_title))
            .setContentText(roomName)
            .setCategory(NotificationCompat.CATEGORY_MISSED_CALL)
            .setAutoCancel(true)
            .setPriority(NotificationCompat.PRIORITY_HIGH)
            .setVisibility(NotificationCompat.VISIBILITY_PRIVATE)
            .setPublicVersion(publicVersion)
            .apply { if (pending != null) setContentIntent(pending) }
            .build()
        runCatching { manager.notify(notificationId, notification) }
            .onFailure { Log.w(TAG, "Missed-call notification failed", it) }
    }

    private fun touchActive(prefs: android.content.SharedPreferences) {
        prefs.edit().putLong(KEY_ACTIVE_AT, System.currentTimeMillis()).apply()
    }

    private fun expireStaleActiveLocked(prefs: android.content.SharedPreferences) {
        val active = prefs.getString(KEY_ACTIVE_TOKEN, "").orEmpty()
        if (active.isEmpty()) return
        val activeAt = prefs.getLong(KEY_ACTIVE_AT, 0L)
        if (activeAt <= 0L || System.currentTimeMillis() - activeAt > ACTIVE_RING_TTL_MS) {
            Log.w(TAG, "Clearing stale active call: $active")
            clearActive(prefs)
        }
    }

    private fun normalizeToken(roomToken: String?, acceptUrl: String?, fallback: String): String {
        extractTokenFromUrl(acceptUrl)?.let { return it }
        roomToken?.trim()?.takeIf { it.isNotBlank() }?.let { return it }
        return "call:${kotlin.math.abs(fallback.hashCode())}"
    }

    private fun extractTokenFromUrl(url: String?): String? {
        if (url.isNullOrBlank()) return null
        val path = runCatching { android.net.Uri.parse(url).path }.getOrNull() ?: url
        val marker = "/call/"
        val idx = path.indexOf(marker)
        if (idx < 0) return null
        val rest = path.substring(idx + marker.length)
        val end = rest.indexOfFirst { it == '/' || it == '?' || it == '#' }.let { if (it < 0) rest.length else it }
        return rest.substring(0, end).ifBlank { null }
    }

    private fun cancelNotification(context: Context) {
        F7IncomingCallRinger.stop()
        context.getSystemService(NotificationManager::class.java)
            ?.cancel(ACTIVE_NOTIFICATION_ID)
    }

    private fun readQueue(prefs: android.content.SharedPreferences): JSONArray {
        val raw = prefs.getString(KEY_QUEUE, "[]").orEmpty()
        return runCatching { JSONArray(raw) }.getOrDefault(JSONArray())
    }

    private fun containsToken(queue: JSONArray, token: String): Boolean {
        for (i in 0 until queue.length()) {
            runCatching {
                if (token == queue.getJSONObject(i).optString("roomToken")) return true
            }
        }
        return false
    }

    private fun removeToken(queue: JSONArray, token: String): JSONArray {
        val next = JSONArray()
        for (i in 0 until queue.length()) {
            runCatching {
                val item = queue.getJSONObject(i)
                if (token != item.optString("roomToken")) {
                    next.put(item)
                }
            }
        }
        return next
    }

    private data class PendingCall(
        val roomToken: String,
        val title: String,
        val body: String,
        val acceptUrl: String?,
        val displayName: String,
    ) {
        fun toJson(): JSONObject = JSONObject().apply {
            put("roomToken", roomToken)
            put("title", title)
            put("body", body)
            put("displayName", displayName)
            if (acceptUrl != null) put("acceptUrl", acceptUrl)
        }

        companion object {
            fun fromJson(o: JSONObject): PendingCall = PendingCall(
                roomToken = o.getString("roomToken"),
                title = o.optString("title", ""),
                body = o.optString("body", ""),
                acceptUrl = if (o.has("acceptUrl")) o.optString("acceptUrl") else null,
                displayName = o.optString("displayName", ""),
            )
        }
    }

    private const val INCOMING_CALL_ACTIVITY = "ru.forbion.f7cloud.mobile.CallIncomingActivity"

    private fun incomingCallIntent(
        context: Context,
        call: PendingCall,
        joinUrl: String,
        autoAccept: Boolean,
    ): Intent = Intent().apply {
        setClassName(context, INCOMING_CALL_ACTIVITY)
        action = PushIntents.ACTION_OPEN_CALL
        addFlags(
            Intent.FLAG_ACTIVITY_NEW_TASK or
                Intent.FLAG_ACTIVITY_CLEAR_TOP or
                Intent.FLAG_ACTIVITY_SINGLE_TOP,
        )
        putExtra(PushIntents.EXTRA_ACCEPT_URL, joinUrl)
        putExtra(PushIntents.EXTRA_ROOM_TOKEN, call.roomToken)
        putExtra(PushIntents.EXTRA_CALL_TITLE, call.title)
        putExtra(PushIntents.EXTRA_CALL_BODY, call.body)
        putExtra(PushIntents.EXTRA_ROOM_DISPLAY_NAME, call.displayName)
        putExtra(PushIntents.EXTRA_AUTO_ACCEPT, autoAccept)
    }
}
