/*
 * TimeLimit Copyright <C> 2019 - 2023 Jonas Lochmann
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
 */
package io.timelimit.android.ui.setup.device

import android.Manifest
import android.os.Build
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.CheckBox
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.widget.AppCompatRadioButton
import androidx.fragment.app.Fragment
import androidx.fragment.app.viewModels
import androidx.lifecycle.*
import io.timelimit.android.R
import io.timelimit.android.coroutines.runAsync
import io.timelimit.android.data.IdGenerator
import io.timelimit.android.data.model.AppRecommendation
import io.timelimit.android.data.model.UserType
import io.timelimit.android.databinding.FragmentSetupDeviceBinding
import io.timelimit.android.livedata.*
import io.timelimit.android.logic.DefaultAppLogic
import io.timelimit.android.ui.main.ActivityViewModelHolder
import io.timelimit.android.ui.main.FragmentWithCustomTitle
import io.timelimit.android.ui.manage.device.manage.advanced.ManageDeviceBackgroundSync
import io.timelimit.android.ui.model.UpdateStateCommand
import io.timelimit.android.ui.model.execute
import io.timelimit.android.ui.mustread.MustReadFragment
import io.timelimit.android.ui.setup.SetupNetworkTimeVerification
import io.timelimit.android.ui.update.UpdateConsentCard
import io.timelimit.android.ui.view.NotifyPermissionCard

class SetupDeviceFragment : Fragment(), FragmentWithCustomTitle {
    companion object {
        private const val PAGE_READY = 0
        private const val PAGE_REQUIRE_AUTH = 1
        private const val PAGE_WORKING = 2
        const val NEW_PARENT = ":np"
        const val NEW_CHILD = ":nc"
        val NEW_USER = setOf(NEW_PARENT, NEW_CHILD)

        private const val STATUS_SELECTED_USER = "a"
        private const val STATUS_SELECTED_APPS_TO_NOT_WHITELIST = "b"
        private const val STATUS_ALLOWED_APPS_CATEGORY = "c"
        private const val STATUS_NOTIFY_PERMISSION = "notify permission"
    }

    private val model: SetupDeviceModel by viewModels()
    private val selectedUser = MutableLiveData<String>()
    private val selectedAppsToNotWhitelist = mutableSetOf<String>()
    private var allowedAppsCategory = ""
    private var notifyPermission = MutableLiveData<NotifyPermissionCard.Status>()

    private val requestNotifyPermission = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted ->
        if (isGranted) notifyPermission.value = NotifyPermissionCard.Status.Granted
        else Toast.makeText(requireContext(), R.string.notify_permission_rejected_toast, Toast.LENGTH_SHORT).show()
    }

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        if (savedInstanceState != null) {
            val oldSelectedUser = savedInstanceState.getString(STATUS_SELECTED_USER)

            oldSelectedUser?.let { selectedUser.value = it }

            selectedAppsToNotWhitelist.clear()
            selectedAppsToNotWhitelist.addAll(
                    savedInstanceState.getStringArrayList(STATUS_SELECTED_APPS_TO_NOT_WHITELIST)!!
            )

            allowedAppsCategory = savedInstanceState.getString(STATUS_ALLOWED_APPS_CATEGORY)!!

            notifyPermission.value = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU)
                savedInstanceState.getSerializable(STATUS_NOTIFY_PERMISSION, NotifyPermissionCard.Status::class.java)!!
            else
                savedInstanceState.getSerializable(STATUS_NOTIFY_PERMISSION)!! as NotifyPermissionCard.Status
        }

        notifyPermission.value = NotifyPermissionCard.updateStatus(notifyPermission.value ?: NotifyPermissionCard.Status.Unknown, requireContext())
    }

    override fun onResume() {
        super.onResume()

        notifyPermission.value = NotifyPermissionCard.updateStatus(notifyPermission.value ?: NotifyPermissionCard.Status.Unknown, requireContext())
    }

    override fun onSaveInstanceState(outState: Bundle) {
        super.onSaveInstanceState(outState)

        outState.putString(STATUS_SELECTED_USER, selectedUser.value)
        outState.putStringArrayList(STATUS_SELECTED_APPS_TO_NOT_WHITELIST, ArrayList(selectedAppsToNotWhitelist))
        outState.putString(STATUS_ALLOWED_APPS_CATEGORY, allowedAppsCategory)
        outState.putSerializable(STATUS_NOTIFY_PERMISSION, notifyPermission.value)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View {
        val binding = FragmentSetupDeviceBinding.inflate(inflater, container, false)
        val logic = DefaultAppLogic.with(requireContext())
        val activity = activity as ActivityViewModelHolder

        binding.needsParent.authBtn.setOnClickListener {
            activity.showAuthenticationScreen()
        }

        val isParentAuthenticatedLive = activity.getActivityViewModel().authenticatedUser.map { it?.second?.type == UserType.Parent }

        binding.flipper.displayedChild = PAGE_WORKING
        mergeLiveData(isParentAuthenticatedLive, model.status).observe(this, Observer { (isParent, status) ->
            if (status == SetupDeviceModelStatus.Ready) {
                if (isParent == true) {
                    binding.flipper.displayedChild = PAGE_READY
                } else {
                    binding.flipper.displayedChild = PAGE_REQUIRE_AUTH
                }
            } else {
                binding.flipper.displayedChild = PAGE_WORKING

                when (status) {
                    SetupDeviceModelStatus.Done -> {
                        runAsync {
                            MustReadFragment.newInstance(R.string.must_read_child_manipulation).show(fragmentManager!!)

                            val ownDeviceId = logic.deviceId.waitForNullableValue()!!

                            requireActivity().execute(UpdateStateCommand.ManageDevice.EnterFromDeviceSetup(ownDeviceId))
                        }
                    }
                    SetupDeviceModelStatus.Working -> { /* nothing to do */ }
                    null -> { /* nothing to do */ }
                    else -> throw IllegalStateException()
                }
            }
        })

        logic.database.user().getAllUsersLive().observe(viewLifecycleOwner) { users ->
            // ID to label
            val items = mutableListOf<Pair<String, String>>()

            // prepare the list content
            users.forEach { user ->
                items.add(user.id to user.name)
            }

            items.add(NEW_PARENT to getString(R.string.setup_device_new_parent))
            items.add(NEW_CHILD to getString(R.string.setup_device_new_child))

            // select the first item if nothing is selected currently
            if (items.find { (id) -> id == selectedUser.value } == null) {
                items.firstOrNull()?.first?.let {
                    selectedUser.value = it
                }
            }

            // build the views
            val views = items.map { (id, label) ->
                AppCompatRadioButton(context).apply {
                    setOnClickListener { selectedUser.value = id }
                    text = label
                    tag = id
                }
            }

            // apply the views
            binding.selectUserRadioGroup.removeAllViews()
            views.forEach { view -> binding.selectUserRadioGroup.addView(view) }
            views.find { it.tag == selectedUser.value }?.isChecked = true
        }

        val isNewUser = selectedUser.map { NEW_USER.contains(it) }
        val isParentUser = selectedUser.switchMap {
            if (it == NEW_CHILD) {
                liveDataFromNonNullValue(false)
            } else if (it == NEW_PARENT) {
                liveDataFromNonNullValue(true)
            } else {
                logic.database.user().getUserByIdLive(it).map {user ->
                    user?.type == UserType.Parent
                }
            }
        }

        isNewUser.observe(viewLifecycleOwner) { binding.isAddingNewUser = it }
        isParentUser.observe(viewLifecycleOwner) { binding.isAddingChild = !it }

        val categoriesOfTheSelectedUser = selectedUser.switchMap { user ->
            if (NEW_USER.contains(user)) {
                liveDataFromNonNullValue(emptyList())
            } else {
                logic.database.category().getCategoriesByChildId(user)
            }
        }

        val appsAssignedToTheUser = categoriesOfTheSelectedUser.switchMap { categories ->
            logic.database.categoryApp().getCategoryApps(categories.map { it.id }).map { categoryApps ->
                categoryApps.map { it.appSpecifierString }.toSet()
            }
        }

        val recommendWhitelistLocalApps = logic.platformIntegration.getLocalApps(IdGenerator.generateId()).filter { it.recommendation == AppRecommendation.Whitelist }

        val appsToWhitelist = appsAssignedToTheUser.map { assignedApps ->
            recommendWhitelistLocalApps.filterNot { app -> assignedApps.contains(app.packageName) }
        }

        appsToWhitelist.observe(viewLifecycleOwner) { apps ->
            binding.areThereAnyApps = apps.isNotEmpty()

            binding.suggestedAllowedApps.removeAllViews()

            apps.forEach { app ->
                binding.suggestedAllowedApps.addView(
                        CheckBox(context).apply {
                            text = app.title
                            isChecked = !selectedAppsToNotWhitelist.contains(app.packageName)

                            setOnCheckedChangeListener { _, isChecked ->
                                if (isChecked) {
                                    selectedAppsToNotWhitelist.remove(app.packageName)
                                } else {
                                    selectedAppsToNotWhitelist.add(app.packageName)
                                }
                            }
                        }
                )
            }
        }

        categoriesOfTheSelectedUser.observe(viewLifecycleOwner) { categories ->
            // id to title
            val items = mutableListOf<Pair<String, String>>()

            categories.forEach { category ->
                items.add(category.id to category.title)
            }

            if (items.isEmpty()) {
                allowedAppsCategory = ""
                binding.areThereAnyCategories = false
            } else {
                if (items.find { (id) -> id == allowedAppsCategory } == null) {
                    // use the one with the lowest blocked times
                    allowedAppsCategory = categories.minByOrNull { it.blockedMinutesInWeek.dataNotToModify.cardinality() }!!.id
                }

                binding.areThereAnyCategories = true

                val views = items.map { (id, label) ->
                    AppCompatRadioButton(requireContext()).apply {
                        text = label
                        tag = id
                        setOnClickListener { allowedAppsCategory = id }
                    }
                }

                binding.allowedAppsCategory.removeAllViews()
                views.forEach { view -> binding.allowedAppsCategory.addView(view) }
                views.find { it.tag == allowedAppsCategory }?.isChecked = true
            }
        }

        val selectedName = MutableLiveData<String>().apply { value = binding.newUserName.text.toString() }
        binding.newUserName.addTextChangedListener(object: TextWatcher {
            override fun afterTextChanged(p0: Editable?) = Unit
            override fun beforeTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) = Unit
            override fun onTextChanged(p0: CharSequence?, p1: Int, p2: Int, p3: Int) {
                selectedName.value = binding.newUserName.text.toString()
            }
        })

        val hasSelectedName = selectedName.map { it.isNotBlank() }
        val isNameRequired = isNewUser
        val validationOfName = hasSelectedName.or(isNameRequired.invert())
        val isPasswordRequired = isNewUser.and(isParentUser).and(logic.fullVersion.isLocalMode.invert())
        val isPasswordValid = binding.setPasswordView.passwordOk
        val isPasswordEmpty = binding.setPasswordView.noPasswordChecked
        val validationOfPassword = isPasswordValid.or(
                isPasswordRequired.invert()
                        .and(isPasswordEmpty)
        )
        val validationOfNotifyPermission = notifyPermission.map { NotifyPermissionCard.canProceed(it) }
        val validationOfAll = ((validationOfName.and(validationOfPassword)).or(isNewUser.invert())).and(validationOfNotifyPermission)

        validationOfAll.observe(viewLifecycleOwner) { binding.confirmBtn.isEnabled = it }

        isPasswordRequired.observe(viewLifecycleOwner) { binding.setPasswordView.allowNoPassword.value = !it }

        ManageDeviceBackgroundSync.bind(
                view = binding.backgroundSync,
                isThisDevice = liveDataFromNonNullValue(true),
                lifecycleOwner = this,
                activityViewModel = activity.getActivityViewModel(),
                fragmentManager = parentFragmentManager
        )

        binding.confirmBtn.setOnClickListener {
            model.doSetup(
                    userId = selectedUser.value!!,
                    username = binding.newUserName.text.toString(),
                    password = binding.setPasswordView.readPassword(),
                    allowedAppsCategory = allowedAppsCategory,
                    appsToNotWhitelist = selectedAppsToNotWhitelist,
                    model = activity.getActivityViewModel(),
                    networkTime = SetupNetworkTimeVerification.readSelection(binding.networkTimeVerification),
                    enableUpdateChecks = binding.update.enableSwitch.isChecked,
                    enableAppListSync = binding.appListSync.enableSwitch.isChecked && (isParentUser.value == false)
            )
        }

        SetupNetworkTimeVerification.prepareHelpButton(binding.networkTimeVerification, parentFragmentManager)

        UpdateConsentCard.bind(
                view = binding.update,
                lifecycleOwner = viewLifecycleOwner,
                database = logic.database
        )

        NotifyPermissionCard.bind(object: NotifyPermissionCard.Listener {
            override fun onGrantClicked() { requestNotifyPermission.launch(Manifest.permission.POST_NOTIFICATIONS) }
            override fun onSkipClicked() { notifyPermission.value = NotifyPermissionCard.Status.SkipGrant }
        }, binding.notifyPermissionCard)

        notifyPermission.observe(viewLifecycleOwner) { NotifyPermissionCard.bind(it, binding.notifyPermissionCard) }

        logic.fullVersion.isLocalMode.observe(viewLifecycleOwner) { binding.isConnectedMode = !it }

        return binding.root
    }

    override fun getCustomTitle(): LiveData<String?> = liveDataFromNullableValue("${getString(R.string.overview_finish_setup_title)} < ${getString(R.string.main_tab_overview)}")
}
