package com.getbubblenow.android.fragment

import android.content.Context
import android.os.Bundle
import android.text.InputType
import android.util.Log
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.view.inputmethod.InputMethodManager
import android.widget.EditText
import android.widget.Toast
import com.google.android.material.snackbar.Snackbar
import com.getbubblenow.android.Application
import com.getbubblenow.android.R
import com.wireguard.android.backend.Tunnel
import com.getbubblenow.android.databinding.TunnelEditorFragmentBinding
import com.getbubblenow.android.fragment.AppListDialogFragment.AppSelectionListener
import com.getbubblenow.android.model.ObservableTunnel
import com.getbubblenow.android.util.BiometricAuthenticator
import com.getbubblenow.android.util.ErrorMessages
import com.getbubblenow.android.viewmodel.ConfigProxy
import com.getbubblenow.android.widget.EdgeToEdge.setUpRoot
import com.getbubblenow.android.widget.EdgeToEdge.setUpScrollingContent
import com.wireguard.config.Config

/**
 * Fragment for editing a WireGuard configuration.
 */
class TunnelEditorFragment : BaseFragment(), AppSelectionListener {
    private var haveShownKeys = false
    private var binding: TunnelEditorFragmentBinding? = null
    private var tunnel: ObservableTunnel? = null
    private fun onConfigLoaded(config: Config) {
        binding?.config = ConfigProxy(config)
    }

    private fun onConfigSaved(savedTunnel: Tunnel, throwable: Throwable?) {
        val message: String
        if (throwable == null) {
            message = getString(R.string.config_save_success, savedTunnel.name)
            Log.d(TAG, message)
            Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
            onFinished()
        } else {
            val error = ErrorMessages[throwable]
            message = getString(R.string.config_save_error, savedTunnel.name, error)
            Log.e(TAG, message, throwable)
            binding?.let {
                Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
            }
        }
    }

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

    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
        inflater.inflate(R.menu.config_editor, menu)
    }

    override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?,
                              savedInstanceState: Bundle?): View? {
        super.onCreateView(inflater, container, savedInstanceState)
        binding = TunnelEditorFragmentBinding.inflate(inflater, container, false)
        binding?.apply {
            executePendingBindings()
            setUpRoot(root as ViewGroup)
            setUpScrollingContent(mainContainer, null)
            privateKeyTextLayout.setEndIconOnClickListener { config?.`interface`?.generateKeyPair() }
        }
        return binding?.root
    }

    override fun onDestroyView() {
        activity?.window?.clearFlags(WindowManager.LayoutParams.FLAG_SECURE)
        binding = null
        super.onDestroyView()
    }

    override fun onSelectedAppsSelected(selectedApps: List<String>, isExcluded: Boolean) {
        requireNotNull(binding) { "Tried to set excluded/included apps while no view was loaded" }
        if (isExcluded) {
            binding!!.config!!.`interface`.includedApplications.clear()
            binding!!.config!!.`interface`.excludedApplications.apply {
                clear()
                addAll(selectedApps)
            }
        } else {
            binding!!.config!!.`interface`.excludedApplications.clear()
            binding!!.config!!.`interface`.includedApplications.apply {
                clear()
                addAll(selectedApps)
            }
        }
    }

    private fun onFinished() {
        // Hide the keyboard; it rarely goes away on its own.
        val activity = activity ?: return
        val focusedView = activity.currentFocus
        if (focusedView != null) {
            val inputManager = activity.getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
            inputManager?.hideSoftInputFromWindow(focusedView.windowToken,
                    InputMethodManager.HIDE_NOT_ALWAYS)
        }
        // Tell the activity to finish itself or go back to the detail view.
        activity.runOnUiThread {
            // TODO(smaeul): Remove this hack when fixing the Config ViewModel
            // The selected tunnel has to actually change, but we have to remember this one.
            val savedTunnel = tunnel
            if (savedTunnel === selectedTunnel) selectedTunnel = null
            selectedTunnel = savedTunnel
        }
    }

    override fun onOptionsItemSelected(item: MenuItem): Boolean {
        if (item.itemId == R.id.menu_action_save) {
            binding ?: return false
            val newConfig = try {
                binding!!.config!!.resolve()
            } catch (e: Exception) {
                val error = ErrorMessages[e]
                val tunnelName = if (tunnel == null) binding!!.name else tunnel!!.name
                val message = getString(R.string.config_save_error, tunnelName, error)
                Log.e(TAG, message, e)
                Snackbar.make(binding!!.mainContainer, error, Snackbar.LENGTH_LONG).show()
                return false
            }
            when {
                tunnel == null -> {
                    Log.d(TAG, "Attempting to create new tunnel " + binding!!.name)
                    val manager = Application.getTunnelManager()
                    manager.create(binding!!.name!!, newConfig).whenComplete(this::onTunnelCreated)
                }
                tunnel!!.name != binding!!.name -> {
                    Log.d(TAG, "Attempting to rename tunnel to " + binding!!.name)
                    tunnel!!.setNameAsync(binding!!.name!!).whenComplete { _, t -> onTunnelRenamed(tunnel!!, newConfig, t) }
                }
                else -> {
                    Log.d(TAG, "Attempting to save config of " + tunnel!!.name)
                    tunnel!!.setConfigAsync(newConfig)
                            .whenComplete { _, t -> onConfigSaved(tunnel!!, t) }
                }
            }
            return true
        }
        return super.onOptionsItemSelected(item)
    }

    @Suppress("UNUSED_PARAMETER")
    fun onRequestSetExcludedIncludedApplications(view: View?) {
        if (binding != null) {
            var isExcluded = true
            var selectedApps = ArrayList(binding!!.config!!.`interface`.excludedApplications)
            if (selectedApps.isEmpty()) {
                selectedApps = ArrayList(binding!!.config!!.`interface`.includedApplications)
                if (selectedApps.isNotEmpty())
                    isExcluded = false
            }
            val fragment = AppListDialogFragment.newInstance(selectedApps, isExcluded, this)
            fragment.show(parentFragmentManager, null)
        }
    }

    override fun onSaveInstanceState(outState: Bundle) {
        if (binding != null) outState.putParcelable(KEY_LOCAL_CONFIG, binding!!.config)
        outState.putString(KEY_ORIGINAL_NAME, if (tunnel == null) null else tunnel!!.name)
        super.onSaveInstanceState(outState)
    }

    override fun onSelectedTunnelChanged(oldTunnel: ObservableTunnel?,
                                         newTunnel: ObservableTunnel?) {
        tunnel = newTunnel
        if (binding == null) return
        binding!!.config = ConfigProxy()
        if (tunnel != null) {
            binding!!.name = tunnel!!.name
            tunnel!!.configAsync.thenAccept(this::onConfigLoaded)
        } else {
            binding!!.name = ""
        }
    }

    private fun onTunnelCreated(newTunnel: ObservableTunnel, throwable: Throwable?) {
        val message: String
        if (throwable == null) {
            tunnel = newTunnel
            message = getString(R.string.tunnel_create_success, tunnel!!.name)
            Log.d(TAG, message)
            Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show()
            onFinished()
        } else {
            val error = ErrorMessages[throwable]
            message = getString(R.string.tunnel_create_error, error)
            Log.e(TAG, message, throwable)
            binding?.let {
                Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
            }
        }
    }

    private fun onTunnelRenamed(renamedTunnel: ObservableTunnel, newConfig: Config,
                                throwable: Throwable?) {
        val message: String
        if (throwable == null) {
            message = getString(R.string.tunnel_rename_success, renamedTunnel.name)
            Log.d(TAG, message)
            // Now save the rest of configuration changes.
            Log.d(TAG, "Attempting to save config of renamed tunnel " + tunnel!!.name)
            renamedTunnel.setConfigAsync(newConfig).whenComplete { _, t -> onConfigSaved(renamedTunnel, t) }
        } else {
            val error = ErrorMessages[throwable]
            message = getString(R.string.tunnel_rename_error, error)
            Log.e(TAG, message, throwable)
            binding?.let {
                Snackbar.make(it.mainContainer, message, Snackbar.LENGTH_LONG).show()
            }
        }
    }

    override fun onViewStateRestored(savedInstanceState: Bundle?) {
        binding ?: return
        binding!!.fragment = this
        if (savedInstanceState == null) {
            onSelectedTunnelChanged(null, selectedTunnel)
        } else {
            tunnel = selectedTunnel
            val config: ConfigProxy = savedInstanceState.getParcelable(KEY_LOCAL_CONFIG)!!
            val originalName = savedInstanceState.getString(KEY_ORIGINAL_NAME)
            if (tunnel != null && tunnel!!.name != originalName) onSelectedTunnelChanged(null, tunnel) else binding!!.config = config
        }
        super.onViewStateRestored(savedInstanceState)
    }

    private var showingAuthenticator = false

    fun onKeyClick(view: View) = onKeyFocusChange(view, true)

    fun onKeyFocusChange(view: View, isFocused: Boolean) {
        if (!isFocused || showingAuthenticator) return
        val edit = view as? EditText ?: return
        if (edit.inputType == InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD) return
        if (!haveShownKeys && edit.text.isNotEmpty()) {
            showingAuthenticator = true
            BiometricAuthenticator.authenticate(R.string.biometric_prompt_private_key_title, this) {
                showingAuthenticator = false
                when (it) {
                    is BiometricAuthenticator.Result.Success, is BiometricAuthenticator.Result.HardwareUnavailableOrDisabled -> {
                        haveShownKeys = true
                        showPrivateKey(edit)
                    }
                    is BiometricAuthenticator.Result.Failure -> {
                        Snackbar.make(
                                binding!!.mainContainer,
                                it.message,
                                Snackbar.LENGTH_SHORT
                        ).show()
                    }
                }
            }
        } else {
            showPrivateKey(edit)
        }
    }

    private fun showPrivateKey(edit: EditText) {
        activity?.window?.addFlags(WindowManager.LayoutParams.FLAG_SECURE)
        edit.inputType = InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS or InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD
    }

    companion object {
        private const val KEY_LOCAL_CONFIG = "local_config"
        private const val KEY_ORIGINAL_NAME = "original_name"
        private const val TAG = "WireGuard/TunnelEditorFragment"
    }
}
