package io.heckel.ntfy.ui

import android.content.ContentResolver
import android.content.ClipData
import android.content.ClipboardManager
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.text.TextUtils
import android.widget.Toast
import androidx.activity.result.ActivityResultLauncher
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.FileProvider
import androidx.core.graphics.drawable.toDrawable
import androidx.lifecycle.lifecycleScope
import androidx.preference.*
import androidx.preference.Preference.OnPreferenceClickListener
import io.heckel.ntfy.BuildConfig
import io.heckel.ntfy.R
import io.heckel.ntfy.db.Repository
import io.heckel.ntfy.db.Subscription
import io.heckel.ntfy.msg.DownloadAttachmentWorker
import io.heckel.ntfy.msg.NotificationService
import io.heckel.ntfy.service.SubscriberServiceManager
import io.heckel.ntfy.util.*
import kotlinx.coroutines.*
import java.io.File
import java.io.IOException
import java.util.*

/**
 * Subscription settings
 */
class DetailSettingsActivity : AppCompatActivity() {
    private lateinit var repository: Repository
    private lateinit var serviceManager: SubscriberServiceManager
    private lateinit var settingsFragment: SettingsFragment
    private lateinit var notificationService: NotificationService
    private var subscriptionId: Long = 0

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_settings)

        Log.d(TAG, "Create $this")

        repository = Repository.getInstance(this)
        serviceManager = SubscriberServiceManager(this)
        notificationService = NotificationService(this)
        subscriptionId = intent.getLongExtra(DetailActivity.EXTRA_SUBSCRIPTION_ID, 0)

        if (savedInstanceState == null) {
            settingsFragment = SettingsFragment() // Empty constructor!
            settingsFragment.arguments = Bundle().apply {
                this.putLong(DetailActivity.EXTRA_SUBSCRIPTION_ID, subscriptionId)
            }
            supportFragmentManager
                .beginTransaction()
                .replace(R.id.settings_layout, settingsFragment)
                .commit()
        }

        // Title
        val displayName = intent.getStringExtra(DetailActivity.EXTRA_SUBSCRIPTION_DISPLAY_NAME) ?: return
        title = displayName

        // Show 'Back' button
        supportActionBar?.setDisplayHomeAsUpEnabled(true)
    }

    override fun onSupportNavigateUp(): Boolean {
        finish() // Return to previous activity when nav "back" is pressed!
        return true
    }

    class SettingsFragment : PreferenceFragmentCompat() {
        private lateinit var resolver: ContentResolver
        private lateinit var repository: Repository
        private lateinit var serviceManager: SubscriberServiceManager
        private lateinit var notificationService: NotificationService
        private lateinit var subscription: Subscription

        private lateinit var iconSetPref: Preference
        private lateinit var openChannelsPref: Preference
        private lateinit var iconSetLauncher: ActivityResultLauncher<String>
        private lateinit var iconRemovePref: Preference

        override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) {
            setPreferencesFromResource(R.xml.detail_preferences, rootKey)

            // Dependencies (Fragments need a default constructor)
            repository = Repository.getInstance(requireActivity())
            serviceManager = SubscriberServiceManager(requireActivity())
            notificationService = NotificationService(requireActivity())
            resolver = requireContext().applicationContext.contentResolver

            // Create result launcher for custom icon (must be created in onCreatePreferences() directly)
            iconSetLauncher = createIconPickLauncher()

            // Load subscription and users
            val subscriptionId = arguments?.getLong(DetailActivity.EXTRA_SUBSCRIPTION_ID) ?: return
            runBlocking {
                withContext(Dispatchers.IO) {
                    subscription = repository.getSubscription(subscriptionId) ?: return@withContext
                    activity?.runOnUiThread {
                        loadView()
                    }
                }
            }
        }

        private fun loadView() {
            if (subscription.upAppId == null) {
                loadMutedUntilPref()
                loadMinPriorityPref()
                loadAutoDeletePref()
                loadInsistentMaxPriorityPref()
                loadIconSetPref()
                loadIconRemovePref()
                if (notificationService.channelsSupported()) {
                    loadDedicatedChannelsPrefs()
                    loadOpenChannelsPrefs()
                }
            } else {
                val notificationsHeaderId = context?.getString(R.string.detail_settings_notifications_header_key) ?: return
                val notificationsHeader: PreferenceCategory? = findPreference(notificationsHeaderId)
                notificationsHeader?.isVisible = false
            }
            loadDisplayNamePref()
            loadTopicUrlPref()
        }

        private fun loadDedicatedChannelsPrefs() {
            val prefId = context?.getString(R.string.detail_settings_notifications_dedicated_channels_key) ?: return
            val pref: SwitchPreference? = findPreference(prefId)
            pref?.isVisible = true
            pref?.isChecked = subscription.dedicatedChannels
            pref?.preferenceDataStore = object : PreferenceDataStore() {
                override fun putBoolean(key: String?, value: Boolean) {
                    save(subscription.copy(dedicatedChannels = value))
                    if (value) {
                        notificationService.createSubscriptionNotificationChannels(subscription)
                    } else {
                        notificationService.deleteSubscriptionNotificationChannels(subscription)
                    }
                    openChannelsPref.isVisible = value
                }
                override fun getBoolean(key: String?, defValue: Boolean): Boolean {
                    return subscription.dedicatedChannels
                }
            }
            pref?.summaryProvider = Preference.SummaryProvider<SwitchPreference> { preference ->
                if (preference.isChecked) {
                    getString(R.string.detail_settings_notifications_dedicated_channels_summary_on)
                } else {
                    getString(R.string.detail_settings_notifications_dedicated_channels_summary_off)
                }
            }
        }

        private fun loadOpenChannelsPrefs() {
            val prefId = context?.getString(R.string.detail_settings_notifications_open_channels_key) ?: return
            openChannelsPref = findPreference(prefId) ?: return
            openChannelsPref.isVisible = subscription.dedicatedChannels
            openChannelsPref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
            openChannelsPref.onPreferenceClickListener = Preference.OnPreferenceClickListener { _ ->
                val settingsIntent = Intent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)
                    .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
                    .putExtra(Settings.EXTRA_APP_PACKAGE, requireContext().applicationContext.packageName)
                startActivity(settingsIntent);
                true
            }
        }

        private fun loadMutedUntilPref() {
            val prefId = context?.getString(R.string.detail_settings_notifications_muted_until_key) ?: return
            val pref: ListPreference? = findPreference(prefId)
            pref?.isVisible = true // Hack: Show all settings at once, because subscription is loaded asynchronously
            pref?.value = subscription.mutedUntil.toString()
            pref?.preferenceDataStore = object : PreferenceDataStore() {
                override fun putString(key: String?, value: String?) {
                    val mutedUntilValue = value?.toLongOrNull() ?:return
                    when (mutedUntilValue) {
                        Repository.MUTED_UNTIL_SHOW_ALL -> save(subscription.copy(mutedUntil = mutedUntilValue))
                        Repository.MUTED_UNTIL_FOREVER -> save(subscription.copy(mutedUntil = mutedUntilValue))
                        Repository.MUTED_UNTIL_TOMORROW -> {
                            val date = Calendar.getInstance()
                            date.add(Calendar.DAY_OF_MONTH, 1)
                            date.set(Calendar.HOUR_OF_DAY, 8)
                            date.set(Calendar.MINUTE, 30)
                            date.set(Calendar.SECOND, 0)
                            date.set(Calendar.MILLISECOND, 0)
                            save(subscription.copy(mutedUntil = date.timeInMillis/1000))
                        }
                        else -> {
                            val mutedUntilTimestamp = System.currentTimeMillis()/1000 + mutedUntilValue * 60
                            save(subscription.copy(mutedUntil = mutedUntilTimestamp))
                        }
                    }
                }
                override fun getString(key: String?, defValue: String?): String {
                    return subscription.mutedUntil.toString()
                }
            }
            pref?.summaryProvider = Preference.SummaryProvider<ListPreference> {
                when (val mutedUntilValue = subscription.mutedUntil) {
                    Repository.MUTED_UNTIL_SHOW_ALL -> getString(R.string.settings_notifications_muted_until_show_all)
                    Repository.MUTED_UNTIL_FOREVER -> getString(R.string.settings_notifications_muted_until_forever)
                    else -> {
                        val formattedDate = formatDateShort(mutedUntilValue)
                        getString(R.string.settings_notifications_muted_until_x, formattedDate)
                    }
                }
            }
        }

        private fun loadMinPriorityPref() {
            val prefId = context?.getString(R.string.detail_settings_notifications_min_priority_key) ?: return
            val pref: ListPreference? = findPreference(prefId)
            pref?.isVisible = true // Hack: Show all settings at once, because subscription is loaded asynchronously
            pref?.value = subscription.minPriority.toString()
            pref?.preferenceDataStore = object : PreferenceDataStore() {
                override fun putString(key: String?, value: String?) {
                    val minPriorityValue = value?.toIntOrNull() ?:return
                    save(subscription.copy(minPriority = minPriorityValue))
                }
                override fun getString(key: String?, defValue: String?): String {
                    return subscription.minPriority.toString()
                }
            }
            pref?.summaryProvider = Preference.SummaryProvider<ListPreference> { preference ->
                var value = preference.value.toIntOrNull() ?: Repository.MIN_PRIORITY_USE_GLOBAL
                val global = value == Repository.MIN_PRIORITY_USE_GLOBAL
                if (value == Repository.MIN_PRIORITY_USE_GLOBAL) {
                    value = repository.getMinPriority()
                }
                val summary = when (value) {
                    PRIORITY_MIN -> getString(R.string.settings_notifications_min_priority_summary_any)
                    PRIORITY_MAX -> getString(R.string.settings_notifications_min_priority_summary_max)
                    else -> {
                        val minPriorityString = toPriorityString(requireContext(), value)
                        getString(R.string.settings_notifications_min_priority_summary_x_or_higher, value, minPriorityString)
                    }
                }
                maybeAppendGlobal(summary, global)
            }
        }

        private fun loadAutoDeletePref() {
            val prefId = context?.getString(R.string.detail_settings_notifications_auto_delete_key) ?: return
            val pref: ListPreference? = findPreference(prefId)
            pref?.isVisible = true // Hack: Show all settings at once, because subscription is loaded asynchronously
            pref?.value = subscription.autoDelete.toString()
            pref?.preferenceDataStore = object : PreferenceDataStore() {
                override fun putString(key: String?, value: String?) {
                    val seconds = value?.toLongOrNull() ?:return
                    save(subscription.copy(autoDelete = seconds))
                }
                override fun getString(key: String?, defValue: String?): String {
                    return subscription.autoDelete.toString()
                }
            }
            pref?.summaryProvider = Preference.SummaryProvider<ListPreference> { preference ->
                var seconds = preference.value.toLongOrNull() ?: Repository.AUTO_DELETE_USE_GLOBAL
                val global = seconds == Repository.AUTO_DELETE_USE_GLOBAL
                if (global) {
                    seconds = repository.getAutoDeleteSeconds()
                }
                val summary = when (seconds) {
                    Repository.AUTO_DELETE_NEVER -> getString(R.string.settings_notifications_auto_delete_summary_never)
                    Repository.AUTO_DELETE_ONE_DAY_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_one_day)
                    Repository.AUTO_DELETE_THREE_DAYS_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_three_days)
                    Repository.AUTO_DELETE_ONE_WEEK_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_one_week)
                    Repository.AUTO_DELETE_ONE_MONTH_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_one_month)
                    Repository.AUTO_DELETE_THREE_MONTHS_SECONDS -> getString(R.string.settings_notifications_auto_delete_summary_three_months)
                    else -> getString(R.string.settings_notifications_auto_delete_summary_one_month) // Must match default const
                }
                maybeAppendGlobal(summary, global)
            }
        }

        private fun loadInsistentMaxPriorityPref() {
            val prefId = context?.getString(R.string.detail_settings_notifications_insistent_max_priority_key) ?: return
            val pref: ListPreference? = findPreference(prefId)
            pref?.isVisible = true
            pref?.value = subscription.insistent.toString()
            pref?.preferenceDataStore = object : PreferenceDataStore() {
                override fun putString(key: String?, value: String?) {
                    val intValue = value?.toIntOrNull() ?:return
                    save(subscription.copy(insistent = intValue))
                }
                override fun getString(key: String?, defValue: String?): String {
                    return subscription.insistent.toString()
                }
            }
            pref?.summaryProvider = Preference.SummaryProvider<ListPreference> { preference ->
                val value = preference.value.toIntOrNull() ?: Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL
                val global = value == Repository.INSISTENT_MAX_PRIORITY_USE_GLOBAL
                val enabled = if (global) repository.getInsistentMaxPriorityEnabled() else value == Repository.INSISTENT_MAX_PRIORITY_ENABLED
                val summary = if (enabled) {
                    getString(R.string.settings_notifications_insistent_max_priority_summary_enabled)
                } else {
                    getString(R.string.settings_notifications_insistent_max_priority_summary_disabled)
                }
                maybeAppendGlobal(summary, global)
            }
        }

        private fun loadIconSetPref() {
            val prefId = context?.getString(R.string.detail_settings_appearance_icon_set_key) ?: return
            iconSetPref = findPreference(prefId) ?: return
            iconSetPref.isVisible = subscription.icon == null
            iconSetPref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
            iconSetPref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                iconSetLauncher.launch("image/*")
                true
            }
        }

        private fun loadIconRemovePref() {
            val prefId = context?.getString(R.string.detail_settings_appearance_icon_remove_key) ?: return
            iconRemovePref = findPreference(prefId) ?: return
            iconRemovePref.isVisible = subscription.icon != null
            iconRemovePref.preferenceDataStore = object : PreferenceDataStore() { } // Dummy store to protect from accidentally overwriting
            iconRemovePref.onPreferenceClickListener = Preference.OnPreferenceClickListener {
                iconRemovePref.isVisible = false
                iconSetPref.isVisible = true
                deleteIcon(subscription.icon)
                save(subscription.copy(icon = null))
                true
            }

            // Set icon (if it exists)
            if (subscription.icon != null) {
                try {
                    val bitmap = subscription.icon!!.readBitmapFromUri(requireContext())
                    iconRemovePref.icon = bitmap.toDrawable(resources)
                } catch (e: Exception) {
                    Log.w(TAG, "Unable to set icon ${subscription.icon}", e)
                }
            }
        }

        private fun loadDisplayNamePref() {
            val prefId = context?.getString(R.string.detail_settings_appearance_display_name_key) ?: return
            val pref: EditTextPreference? = findPreference(prefId)
            pref?.isVisible = true // Hack: Show all settings at once, because subscription is loaded asynchronously
            pref?.text = subscription.displayName
            pref?.dialogMessage = getString(R.string.detail_settings_appearance_display_name_message, topicShortUrl(subscription.baseUrl, subscription.topic))
            pref?.preferenceDataStore = object : PreferenceDataStore() {
                override fun putString(key: String?, value: String?) {
                    val displayName = if (value != "") value else null
                    val newSubscription = subscription.copy(displayName = displayName)
                    save(newSubscription)
                    // Update activity title
                    activity?.runOnUiThread {
                        activity?.title = displayName(newSubscription)
                    }
                    // Update dedicated notification channel
                    if (newSubscription.dedicatedChannels) {
                        notificationService.createSubscriptionNotificationChannels(newSubscription)
                    }
                }
                override fun getString(key: String?, defValue: String?): String {
                    return subscription.displayName ?: ""
                }
            }
            pref?.summaryProvider = Preference.SummaryProvider<EditTextPreference> { provider ->
                if (TextUtils.isEmpty(provider.text)) {
                    getString(
                        R.string.detail_settings_appearance_display_name_default_summary,
                        displayName(subscription)
                    )
                } else {
                    provider.text
                }
            }
        }

        private fun loadTopicUrlPref() {
            // Topic URL
            val topicUrlPrefId = context?.getString(R.string.detail_settings_about_topic_url_key) ?: return
            val topicUrlPref: Preference? = findPreference(topicUrlPrefId)
            val topicUrl = topicShortUrl(subscription.baseUrl, subscription.topic)
            topicUrlPref?.summary = topicUrl
            topicUrlPref?.onPreferenceClickListener = OnPreferenceClickListener {
                val context = context ?: return@OnPreferenceClickListener false
                val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
                val clip = ClipData.newPlainText("topic url", topicUrl)
                clipboard.setPrimaryClip(clip)
                Toast
                        .makeText(context, getString(R.string.detail_settings_about_topic_url_copied_to_clipboard_message), Toast.LENGTH_LONG)
                        .show()
                true
            }
        }

        private fun createIconPickLauncher(): ActivityResultLauncher<String> {
            return registerForActivityResult(ActivityResultContracts.GetContent()) { inputUri ->
                if (inputUri == null) {
                    return@registerForActivityResult
                }
                lifecycleScope.launch(Dispatchers.IO) {
                    val outputUri = createUri() ?: return@launch
                    try {
                        // Early size & mime type check
                        val mimeType = resolver.getType(inputUri)
                        if (!supportedImage(mimeType)) {
                            throw IOException("unknown image type or not supported")
                        }
                        val stat = fileStat(requireContext(), inputUri) // May throw
                        if (stat.size > SUBSCRIPTION_ICON_MAX_SIZE_BYTES) {
                            throw IOException("image too large, max supported is ${SUBSCRIPTION_ICON_MAX_SIZE_BYTES/1024/1024}MB")
                        }

                        // Write to cache storage
                        val inputStream = resolver.openInputStream(inputUri) ?: throw IOException("Couldn't open content URI for reading")
                        val outputStream = resolver.openOutputStream(outputUri) ?: throw IOException("Couldn't open content URI for writing")
                        inputStream.use {
                            it.copyTo(outputStream)
                        }

                        // Read image, check dimensions
                        val bitmap = outputUri.readBitmapFromUri(requireContext())
                        if (bitmap.width > SUBSCRIPTION_ICON_MAX_WIDTH || bitmap.height > SUBSCRIPTION_ICON_MAX_HEIGHT) {
                            throw IOException("image exceeds max dimensions of ${SUBSCRIPTION_ICON_MAX_WIDTH}x${SUBSCRIPTION_ICON_MAX_HEIGHT}")
                        }

                        // Display "remove" preference
                        iconRemovePref.icon = bitmap.toDrawable(resources)
                        iconRemovePref.isVisible = true
                        iconSetPref.isVisible = false

                        // Finally, save (this is last!)
                        save(subscription.copy(icon = outputUri.toString()))
                    } catch (e: Exception) {
                        Log.w(TAG, "Saving icon failed", e)
                        requireActivity().runOnUiThread {
                            Toast.makeText(context, getString(R.string.detail_settings_appearance_icon_error_saving, e.message), Toast.LENGTH_LONG).show()
                        }
                    }
                }
            }
        }

        private fun createUri(): Uri? {
            val dir = File(requireContext().cacheDir, SUBSCRIPTION_ICONS)
            if (!dir.exists() && !dir.mkdirs()) {
                return null
            }
            val file =  File(dir, subscription.id.toString())
            return FileProvider.getUriForFile(requireContext(), DownloadAttachmentWorker.FILE_PROVIDER_AUTHORITY, file)
        }

        private fun deleteIcon(uri: String?) {
            if (uri == null) {
                return
            }
            try {
                resolver.delete(Uri.parse(uri), null, null)
            } catch (e: Exception) {
                Log.w(TAG, "Unable to delete $uri", e)
            }
        }

        private fun save(newSubscription: Subscription, refresh: Boolean = false) {
            subscription = newSubscription
            lifecycleScope.launch(Dispatchers.IO) {
                repository.updateSubscription(newSubscription)
                if (refresh) {
                    SubscriberServiceManager.refresh(requireContext())
                }
            }
        }

        private fun maybeAppendGlobal(summary: String, global: Boolean): String {
            return if (global) {
                summary + " (" + getString(R.string.detail_settings_global_setting_suffix) + ")"
            } else {
                summary
            }
        }
    }

    companion object {
        private const val TAG = "NtfyDetailSettingsActiv"
        private const val SUBSCRIPTION_ICONS = "subscriptionIcons"
        private const val SUBSCRIPTION_ICON_MAX_SIZE_BYTES = 4194304
        private const val SUBSCRIPTION_ICON_MAX_WIDTH = 2048
        private const val SUBSCRIPTION_ICON_MAX_HEIGHT = 2048
    }
}