/*
 * Copyright 2024 Signal Messenger, LLC
 * SPDX-License-Identifier: AGPL-3.0-only
 */

package org.thoughtcrime.securesms.components.settings.app.changenumber

import android.os.Bundle
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.EditorInfo
import android.widget.TextView
import android.widget.Toast
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.widget.Toolbar
import androidx.fragment.app.activityViewModels
import androidx.navigation.fragment.findNavController
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import org.signal.core.ui.logging.LoggingFragment
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.R
import org.thoughtcrime.securesms.components.ViewBinderDelegate
import org.thoughtcrime.securesms.components.settings.app.changenumber.ChangeNumberUtil.changeNumberSuccess
import org.thoughtcrime.securesms.databinding.FragmentRegistrationLockBinding
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.lock.v2.SvrConstants
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.fragments.RegistrationViewDelegate
import org.thoughtcrime.securesms.util.CommunicationActions
import org.thoughtcrime.securesms.util.SupportEmailUtil
import org.thoughtcrime.securesms.util.SystemWindowInsetsSetter
import org.thoughtcrime.securesms.util.ViewUtil
import org.thoughtcrime.securesms.util.navigation.safeNavigate
import org.whispersystems.signalservice.api.kbs.PinHashUtil
import java.util.concurrent.TimeUnit

/**
 * Screen presented to the user if the new account is registration locked, and allows them to enter their PIN.
 */
class ChangeNumberRegistrationLockFragment : LoggingFragment(R.layout.fragment_change_number_registration_lock) {

  companion object {
    private val TAG = Log.tag(ChangeNumberRegistrationLockFragment::class.java)
  }

  private val binding: FragmentRegistrationLockBinding by ViewBinderDelegate(bindingFactory = { rootView ->
    FragmentRegistrationLockBinding.bind(rootView.findViewById(R.id.registration_lock_content))
  })

  private val viewModel by activityViewModels<ChangeNumberViewModel>()

  private var timeRemaining: Long = 0

  override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    SystemWindowInsetsSetter.attach(view, viewLifecycleOwner, SystemWindowInsetsSetter.SAFE_AREA_WITH_KEYBOARD)
    RegistrationViewDelegate.setDebugLogSubmitMultiTapView(view.findViewById(R.id.kbs_lock_pin_title))
    val toolbar: Toolbar = view.findViewById(R.id.toolbar)
    toolbar.setNavigationOnClickListener { navigateUp() }

    requireActivity().onBackPressedDispatcher.addCallback(
      viewLifecycleOwner,
      object : OnBackPressedCallback(true) {
        override fun handleOnBackPressed() {
          navigateUp()
        }
      }
    )

    val args: ChangeNumberRegistrationLockFragmentArgs = ChangeNumberRegistrationLockFragmentArgs.fromBundle(requireArguments())

    timeRemaining = args.getTimeRemaining()

    binding.kbsLockForgotPin.visibility = View.GONE
    binding.kbsLockForgotPin.setOnClickListener { handleForgottenPin(timeRemaining) }

    binding.kbsLockPinInput.setImeOptions(EditorInfo.IME_ACTION_DONE)
    binding.kbsLockPinInput.setOnEditorActionListener { v: TextView?, actionId: Int, _: KeyEvent? ->
      if (actionId == EditorInfo.IME_ACTION_DONE) {
        ViewUtil.hideKeyboard(requireContext(), v!!)
        handlePinEntry()
        return@setOnEditorActionListener true
      }
      false
    }

    enableAndFocusPinEntry()

    binding.kbsLockPinConfirm.setOnClickListener {
      ViewUtil.hideKeyboard(requireContext(), binding.kbsLockPinInput)
      handlePinEntry()
    }

    binding.kbsLockKeyboardToggle.setOnClickListener { viewModel.togglePinKeyboardType() }

    viewModel.liveLockedTimeRemaining.observe(viewLifecycleOwner) { t: Long -> timeRemaining = t }

    val triesRemaining: Int = viewModel.svrTriesRemaining

    if (triesRemaining <= 3) {
      val daysRemaining = getLockoutDays(timeRemaining)

      MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.RegistrationLockFragment__not_many_tries_left)
        .setMessage(getTriesRemainingDialogMessage(triesRemaining, daysRemaining))
        .setPositiveButton(android.R.string.ok, null)
        .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, _ -> sendEmailToSupport() }
        .show()
    }

    if (triesRemaining < 5) {
      binding.kbsLockPinInputLabel.text = requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__d_attempts_remaining, triesRemaining, triesRemaining)
    }

    viewModel.uiState.observe(viewLifecycleOwner, ::onStateUpdate)
  }

  private fun onStateUpdate(state: ChangeNumberState) {
    if (state.changeNumberOutcome == ChangeNumberOutcome.Succeeded) {
      handleSuccessfulPinEntry(state.enteredPin)
    }

    state.pinKeyboardType.applyTo(
      pinEditText = binding.kbsLockPinInput,
      toggleTypeButton = binding.kbsLockKeyboardToggle
    )
  }

  private fun handlePinEntry() {
    binding.kbsLockPinInput.setEnabled(false)

    val pin: String = binding.kbsLockPinInput.getText().toString()

    val trimmedLength = pin.replace(" ", "").length
    if (trimmedLength == 0) {
      Toast.makeText(requireContext(), R.string.RegistrationActivity_you_must_enter_your_registration_lock_PIN, Toast.LENGTH_LONG).show()
      enableAndFocusPinEntry()
      return
    }

    if (trimmedLength < SvrConstants.MINIMUM_PIN_LENGTH) {
      Toast.makeText(requireContext(), getString(R.string.RegistrationActivity_your_pin_has_at_least_d_digits_or_characters, SvrConstants.MINIMUM_PIN_LENGTH), Toast.LENGTH_LONG).show()
      enableAndFocusPinEntry()
      return
    }

    viewModel.setEnteredPin(pin)

    binding.kbsLockPinConfirm.setSpinning()
    viewModel.verifyCodeAndRegisterAccountWithRegistrationLock(requireContext(), pin, ::handleSessionErrorResponse, ::handleChangeNumberErrorResponse)
  }

  private fun handleSessionErrorResponse(requestResult: VerificationCodeRequestResult) {
    when (requestResult) {
      is VerificationCodeRequestResult.Success -> Unit
      is VerificationCodeRequestResult.RateLimited -> onRateLimited()
      is VerificationCodeRequestResult.RegistrationLocked -> {
        navigateToAccountLocked()
      }

      is VerificationCodeRequestResult.AlreadyVerified,
      is VerificationCodeRequestResult.ChallengeRequired,
      is VerificationCodeRequestResult.ExternalServiceFailure,
      is VerificationCodeRequestResult.ImpossibleNumber,
      is VerificationCodeRequestResult.InvalidTransportModeFailure,
      is VerificationCodeRequestResult.MalformedRequest,
      is VerificationCodeRequestResult.RequestVerificationCodeRateLimited,
      is VerificationCodeRequestResult.SubmitVerificationCodeRateLimited,
      is VerificationCodeRequestResult.NoSuchSession,
      is VerificationCodeRequestResult.NonNormalizedNumber,
      is VerificationCodeRequestResult.TokenNotAccepted,
      is VerificationCodeRequestResult.UnknownError -> {
        Log.w(TAG, "Unable to verify code with registration lock", requestResult.getCause())
        onError()
      }
    }
  }

  private fun handleChangeNumberErrorResponse(result: ChangeNumberResult) {
    when (result) {
      is ChangeNumberResult.Success -> Unit
      is ChangeNumberResult.RateLimited -> onRateLimited()
      is ChangeNumberResult.AttemptsExhausted -> navigateToAccountLocked()

      is ChangeNumberResult.SvrWrongPin -> {
        Log.i(TAG, "SVR returned a WrongPinException.")
        onIncorrectKbsRegistrationLockPin(result.triesRemaining)
      }

      is ChangeNumberResult.SvrNoData -> {
        Log.i(TAG, "SVR returned a NoDataException.")
        navigateToAccountLocked()
      }

      is ChangeNumberResult.AuthorizationFailed,
      is ChangeNumberResult.IncorrectRecoveryPassword,
      is ChangeNumberResult.MalformedRequest,
      is ChangeNumberResult.RegistrationLocked,
      is ChangeNumberResult.UnknownError,
      is ChangeNumberResult.ValidationError -> {
        Log.w(TAG, "Unable to register account with registration lock", result.getCause())
        onError()
      }
    }
  }

  private fun onIncorrectKbsRegistrationLockPin(svrTriesRemaining: Int) {
    binding.kbsLockPinConfirm.cancelSpinning()
    binding.kbsLockPinInput.getText()?.clear()
    enableAndFocusPinEntry()

    if (svrTriesRemaining == 0) {
      Log.w(TAG, "Account locked. User out of attempts on KBS.")
      navigateToAccountLocked()
      return
    }

    if (svrTriesRemaining == 3) {
      val daysRemaining = getLockoutDays(timeRemaining)

      MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.RegistrationLockFragment__incorrect_pin)
        .setMessage(getTriesRemainingDialogMessage(svrTriesRemaining, daysRemaining))
        .setPositiveButton(android.R.string.ok, null)
        .show()
    }

    if (svrTriesRemaining > 5) {
      binding.kbsLockPinInputLabel.setText(R.string.RegistrationLockFragment__incorrect_pin_try_again)
    } else {
      binding.kbsLockPinInputLabel.text = requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__incorrect_pin_d_attempts_remaining, svrTriesRemaining, svrTriesRemaining)
      binding.kbsLockForgotPin.visibility = View.VISIBLE
    }
  }

  private fun onRateLimited() {
    binding.kbsLockPinConfirm.cancelSpinning()
    enableAndFocusPinEntry()

    MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.RegistrationActivity_too_many_attempts)
      .setMessage(R.string.RegistrationActivity_you_have_made_too_many_incorrect_registration_lock_pin_attempts_please_try_again_in_a_day)
      .setPositiveButton(android.R.string.ok, null)
      .show()
  }

  fun onError() {
    binding.kbsLockPinConfirm.cancelSpinning()
    enableAndFocusPinEntry()

    Toast.makeText(requireContext(), R.string.RegistrationActivity_error_connecting_to_service, Toast.LENGTH_LONG).show()
  }

  private fun handleForgottenPin(timeRemainingMs: Long) {
    val lockoutDays = getLockoutDays(timeRemainingMs)
    MaterialAlertDialogBuilder(requireContext()).setTitle(R.string.RegistrationLockFragment__forgot_your_pin)
      .setMessage(requireContext().resources.getQuantityString(R.plurals.RegistrationLockFragment__for_your_privacy_and_security_there_is_no_way_to_recover, lockoutDays, lockoutDays))
      .setPositiveButton(android.R.string.ok, null)
      .setNeutralButton(R.string.PinRestoreEntryFragment_contact_support) { _, which -> sendEmailToSupport() }
      .show()
  }

  private fun getLockoutDays(timeRemainingMs: Long): Int {
    return TimeUnit.MILLISECONDS.toDays(timeRemainingMs).toInt() + 1
  }

  private fun getTriesRemainingDialogMessage(triesRemaining: Int, daysRemaining: Int): String {
    val resources = requireContext().resources
    val tries = resources.getQuantityString(R.plurals.RegistrationLockFragment__you_have_d_attempts_remaining, triesRemaining, triesRemaining)
    val days = resources.getQuantityString(R.plurals.RegistrationLockFragment__if_you_run_out_of_attempts_your_account_will_be_locked_for_d_days, daysRemaining, daysRemaining)

    return "$tries $days"
  }

  private fun enableAndFocusPinEntry() {
    binding.kbsLockPinInput.setEnabled(true)
    binding.kbsLockPinInput.setFocusable(true)
    ViewUtil.focusAndShowKeyboard(binding.kbsLockPinInput)
  }

  private fun navigateToAccountLocked() {
    findNavController().safeNavigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberAccountLocked())
  }

  private fun handleSuccessfulPinEntry(pin: String) {
    val pinsDiffer: Boolean = SignalStore.svr.localPinHash?.let { !PinHashUtil.verifyLocalPinHash(it, pin) } ?: false

    binding.kbsLockPinConfirm.cancelSpinning()

    if (pinsDiffer) {
      findNavController().safeNavigate(ChangeNumberRegistrationLockFragmentDirections.actionChangeNumberRegistrationLockToChangeNumberPinDiffers())
    } else {
      changeNumberSuccess()
    }
  }

  private fun sendEmailToSupport() {
    val subject = R.string.ChangeNumberRegistrationLockFragment__signal_change_number_need_help_with_pin_for_android_v2_pin

    val body: String = SupportEmailUtil.generateSupportEmailBody(requireContext(), subject, null, null)

    CommunicationActions.openEmail(requireContext(), SupportEmailUtil.getSupportEmailAddress(requireContext()), getString(subject), body)
  }

  private fun navigateUp() {
    if (SignalStore.misc.isChangeNumberLocked) {
      startActivity(ChangeNumberLockActivity.createIntent(requireContext()))
    } else {
      findNavController().navigateUp()
    }
  }
}
