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

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

import android.content.Context
import androidx.lifecycle.ViewModel
import androidx.lifecycle.asLiveData
import androidx.lifecycle.viewModelScope
import com.google.i18n.phonenumbers.NumberParseException
import com.google.i18n.phonenumbers.PhoneNumberUtil
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import org.signal.core.models.ServiceId
import org.signal.core.util.logging.Log
import org.thoughtcrime.securesms.dependencies.AppDependencies
import org.thoughtcrime.securesms.keyvalue.SignalStore
import org.thoughtcrime.securesms.registration.data.RegistrationData
import org.thoughtcrime.securesms.registration.data.RegistrationRepository
import org.thoughtcrime.securesms.registration.data.network.Challenge
import org.thoughtcrime.securesms.registration.data.network.RegistrationSessionCreationResult
import org.thoughtcrime.securesms.registration.data.network.SessionMetadataResult
import org.thoughtcrime.securesms.registration.data.network.VerificationCodeRequestResult
import org.thoughtcrime.securesms.registration.sms.SmsRetrieverReceiver
import org.thoughtcrime.securesms.registration.ui.RegistrationViewModel
import org.thoughtcrime.securesms.registration.ui.countrycode.Country
import org.thoughtcrime.securesms.registration.viewmodel.NumberViewState
import org.thoughtcrime.securesms.registration.viewmodel.SvrAuthCredentialSet
import org.thoughtcrime.securesms.util.dualsim.MccMncProducer
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean

/**
 * [ViewModel] for the change number flow.
 *
 * @see [RegistrationViewModel], from which this is derived.
 */
class ChangeNumberViewModel : ViewModel() {

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

  private val repository = ChangeNumberRepository()
  private val store = MutableStateFlow(ChangeNumberState())
  private val smsRetrieverReceiver: SmsRetrieverReceiver = SmsRetrieverReceiver(AppDependencies.application)

  private val initialLocalNumber = SignalStore.account.e164
  private val password = SignalStore.account.servicePassword!!

  private val changeNumberSessionInFlight = AtomicBoolean(false)

  val uiState = store.asLiveData()
  val liveOldNumberState = store.map { it.oldPhoneNumber }.asLiveData()
  val liveNewNumberState = store.map { it.number }.asLiveData()
  val liveLockedTimeRemaining = store.map { it.lockedTimeRemaining }.asLiveData()
  val incorrectCodeAttempts = store.map { it.incorrectCodeAttempts }.asLiveData()

  init {
    try {
      val countryCode: Int = PhoneNumberUtil.getInstance()
        .parse(SignalStore.account.e164!!, null)
        .countryCode

      store.update {
        it.copy(
          number = it.number.toBuilder().countryCode(countryCode).build(),
          oldPhoneNumber = it.oldPhoneNumber.toBuilder().countryCode(countryCode).build()
        )
      }
    } catch (e: NumberParseException) {
      Log.i(TAG, "Unable to parse number for default country code")
    }

    smsRetrieverReceiver.registerReceiver()
  }

  override fun onCleared() {
    super.onCleared()
    smsRetrieverReceiver.unregisterReceiver()
  }

  // region Public Getters and Setters

  val number: NumberViewState
    get() = store.value.number

  val oldNumberState: NumberViewState
    get() = store.value.oldPhoneNumber

  val svrTriesRemaining: Int
    get() = store.value.svrTriesRemaining

  val oldCountry: Country?
    get() = store.value.oldCountry

  val newCountry: Country?
    get() = store.value.newCountry

  fun setOldNationalNumber(updatedNumber: String) {
    store.update {
      it.copy(oldPhoneNumber = oldNumberState.toBuilder().nationalNumber(updatedNumber).build())
    }
  }

  fun setOldCountry(country: Country) {
    store.update {
      it.copy(
        oldPhoneNumber = oldNumberState.toBuilder().selectedCountryDisplayName(country.name).countryCode(country.countryCode).build(),
        oldCountry = country
      )
    }
  }

  fun setNewNationalNumber(updatedNumber: String) {
    store.update {
      it.copy(number = number.toBuilder().nationalNumber(updatedNumber).build())
    }
  }

  fun setNewCountry(country: Country) {
    store.update {
      it.copy(
        number = number.toBuilder().selectedCountryDisplayName(country.name).countryCode(country.countryCode).build(),
        newCountry = country
      )
    }
  }

  fun setCaptchaResponse(token: String) {
    Log.v(TAG, "setCaptchaResponse()")
    store.update {
      it.copy(captchaToken = token)
    }
  }

  fun setEnteredPin(pin: String) {
    store.update {
      it.copy(enteredPin = pin)
    }
  }

  fun togglePinKeyboardType() {
    store.update {
      it.copy(pinKeyboardType = it.pinKeyboardType.other)
    }
  }

  fun incrementIncorrectCodeAttempts() {
    store.update {
      it.copy(incorrectCodeAttempts = it.incorrectCodeAttempts + 1)
    }
  }

  fun resetLocalSessionState() {
    Log.v(TAG, "resetLocalSessionState()")
    store.update {
      it.copy(inProgress = false, sessionId = null, changeNumberOutcome = null, captchaToken = null, challengesRequested = emptyList(), allowedToRequestCode = false)
    }
  }

  fun canContinue(): ContinueStatus {
    return if (oldNumberState.e164Number == initialLocalNumber) {
      if (number.isValid) {
        ContinueStatus.CAN_CONTINUE
      } else {
        ContinueStatus.INVALID_NUMBER
      }
    } else {
      ContinueStatus.OLD_NUMBER_DOESNT_MATCH
    }
  }

  // endregion

  // region Public actions

  fun reattemptChangeLocalNumber(onSuccess: () -> Unit, onError: (Throwable) -> Unit) {
    require(SignalStore.misc.pendingChangeNumberMetadata != null)
    val metadata = SignalStore.misc.pendingChangeNumberMetadata!!

    Log.v(TAG, "reattemptChangeLocalNumber()")
    viewModelScope.launch(Dispatchers.IO) {
      try {
        val whoAmI = repository.whoAmI()
        val remoteE164 = whoAmI.number
        val remotePni = ServiceId.PNI.parseOrThrow(whoAmI.pni)

        val remoteMatchesPrevious = remoteE164 == metadata.previousE164
        val remoteMatchesNew = remoteE164 == metadata.newE164
        val remoteMatchesLocal = remoteE164 == SignalStore.account.requireE164()

        val reattempt = if (remoteMatchesLocal && remoteMatchesPrevious) {
          Log.i(TAG, "Remote matches local and previous, no state has changed on server or local, can skip reattempt")
          false
        } else if (remoteMatchesNew) {
          Log.i(TAG, "Remote matches new, should be safe to reattempt set local number")
          true
        } else {
          Log.w(TAG, "Unexpected e164 state, remoteMatchesPrevious:$remoteMatchesPrevious remoteMatchesNew:$remoteMatchesNew remoteMatchesLocal:$remoteMatchesLocal")
          withContext(Dispatchers.Main) {
            onError(IllegalStateException("Unexpected e164 state"))
          }
          return@launch
        }

        if (reattempt) {
          Log.i(TAG, "Reattempting local change.\n  Local state  (e164=${SignalStore.account.e164}, pni=${SignalStore.account.pni})\n  Remote state (e164=${whoAmI.number}, pni=$remotePni)")
          repository.changeLocalNumber(whoAmI.number, remotePni)
        }

        SignalStore.misc.unlockChangeNumber()

        withContext(Dispatchers.Main) {
          onSuccess()
        }
      } catch (ioException: IOException) {
        Log.w(TAG, "Encountered an exception when reattempting change local number", ioException)
        withContext(Dispatchers.Main) {
          onError(ioException)
        }
      }
    }
  }

  fun registerSmsListenerWithCompletionListener(context: Context, onComplete: (Boolean) -> Unit) {
    Log.v(TAG, "registerSmsListenerWithCompletionListener()")
    viewModelScope.launch {
      val listenerRegistered = RegistrationRepository.registerSmsListener(context)
      onComplete(listenerRegistered)
    }
  }

  fun verifyCodeWithoutRegistrationLock(context: Context, code: String, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) {
    Log.v(TAG, "verifyCodeWithoutRegistrationLock()")
    store.update {
      it.copy(
        inProgress = true,
        enteredCode = code
      )
    }

    viewModelScope.launch(Dispatchers.IO) {
      verifyCodeInternal(context = context, pin = null, verificationErrorHandler = verificationErrorHandler, numberChangeErrorHandler = numberChangeErrorHandler)
    }
  }

  fun verifyCodeAndRegisterAccountWithRegistrationLock(context: Context, pin: String, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) {
    Log.v(TAG, "verifyCodeAndRegisterAccountWithRegistrationLock()")
    store.update { it.copy(inProgress = true) }

    viewModelScope.launch(Dispatchers.IO) {
      verifyCodeInternal(context = context, pin = pin, verificationErrorHandler = verificationErrorHandler, numberChangeErrorHandler = numberChangeErrorHandler)
    }
  }

  private suspend fun verifyCodeInternal(context: Context, pin: String?, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) {
    val session = getOrCreateValidSession(context) ?: return bail { Log.i(TAG, "Bailing from code verification due to invalid session.") }
    val sessionId = session.sessionId

    if (!session.verified) {
      if (store.value.enteredCode == null) {
        Log.w(TAG, "Session is not verified and no code is available to submit; cannot complete change number.")
        handleVerificationError(VerificationCodeRequestResult.UnknownError(IllegalStateException("No verification code available for an unverified session")), verificationErrorHandler)
        return bail { Log.i(TAG, "Bailing from code verification due to missing code for unverified session.") }
      }

      val registrationData = getRegistrationData(context)

      val verificationResponse = RegistrationRepository.submitVerificationCode(context, sessionId, registrationData)

      if (verificationResponse !is VerificationCodeRequestResult.Success && verificationResponse !is VerificationCodeRequestResult.AlreadyVerified) {
        handleVerificationError(verificationResponse, verificationErrorHandler)
        return bail { Log.i(TAG, "Bailing from code verification due to non-successful response.") }
      }
    }

    val result: ChangeNumberResult = if (pin == null) {
      repository.changeNumberWithoutRegistrationLock(sessionId = sessionId, newE164 = number.e164Number)
    } else {
      repository.changeNumberWithRegistrationLock(
        sessionId = sessionId,
        newE164 = number.e164Number,
        pin = pin,
        svrAuthCredentials = SvrAuthCredentialSet(
          svr2Credentials = store.value.svr2Credentials,
          svr3Credentials = store.value.svr3Credentials
        )
      )
    }

    if (result is ChangeNumberResult.Success) {
      handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.Succeeded)
    } else {
      handleChangeNumberError(result, numberChangeErrorHandler)
    }
  }

  fun submitCaptchaToken(context: Context) {
    Log.v(TAG, "submitCaptchaToken()")
    val e164 = number.e164Number
    val captchaToken = store.value.captchaToken ?: throw IllegalStateException("Can't submit captcha token if no captcha token is set!")
    store.update {
      it.copy(
        captchaToken = null,
        inProgress = true,
        changeNumberOutcome = null,
        challengeInProgress = true
      )
    }

    viewModelScope.launch {
      Log.d(TAG, "Getting session in order to submit captcha token…")
      val sessionData = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing captcha token submission due to invalid session.") }
      if (!sessionData.challengesRequested.contains(Challenge.CAPTCHA)) {
        Log.d(TAG, "Captcha submission no longer necessary, bailing.")
        store.update {
          it.copy(
            inProgress = false,
            changeNumberOutcome = null,
            challengeInProgress = false
          )
        }
        return@launch
      }
      Log.d(TAG, "Submitting captcha token…")
      val captchaSubmissionResult = RegistrationRepository.submitCaptchaToken(context, e164, password, sessionData.sessionId, captchaToken)
      Log.d(TAG, "Captcha token submitted.")
      store.update {
        it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(captchaSubmissionResult), challengeInProgress = false)
      }
    }
  }

  fun requestAndSubmitPushToken(context: Context) {
    Log.v(TAG, "validatePushToken()")

    val e164 = number.e164Number

    viewModelScope.launch {
      Log.d(TAG, "Getting session in order to perform push token verification…")
      val sessionData = getOrCreateValidSession(context) ?: return@launch bail { Log.i(TAG, "Bailing from push token verification due to invalid session.") }

      if (!sessionData.challengesRequested.contains(Challenge.PUSH)) {
        Log.d(TAG, "Push submission no longer necessary, bailing.")
        store.update {
          it.copy(
            inProgress = false,
            changeNumberOutcome = null
          )
        }
        return@launch
      }

      Log.d(TAG, "Requesting push challenge token…")
      val pushSubmissionResult = RegistrationRepository.requestAndVerifyPushToken(context, sessionData.sessionId, e164, password)
      Log.d(TAG, "Push challenge token submitted.")
      store.update {
        it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(pushSubmissionResult))
      }
    }
  }

  fun initiateChangeNumberSession(context: Context, mode: RegistrationRepository.E164VerificationMode) {
    Log.v(TAG, "changeNumber()")
    if (!changeNumberSessionInFlight.compareAndSet(false, true)) {
      Log.i(TAG, "A change number session is already in progress; ignoring duplicate request.")
      return
    }
    store.update { it.copy(inProgress = true) }
    viewModelScope.launch(Dispatchers.IO) {
      try {
        val encryptionDrained = repository.ensureDecryptionsDrained() ?: false

        if (!encryptionDrained) {
          return@launch bail { Log.i(TAG, "Failed to drain encryption.") }
        }

        when (changeNumberWithRecoveryPassword()) {
          ChangeLocalNumberOutcome.NotPerformed -> requestVerificationCode(context, mode)
          ChangeLocalNumberOutcome.Success -> Log.d(TAG, "Successfully changed number using recovery password")
          ChangeLocalNumberOutcome.Failure -> Log.w(TAG, "Change number failed, bailing")
        }
      } finally {
        changeNumberSessionInFlight.set(false)
      }
    }
  }

  // endregion

  // region Private actions

  private fun updateLocalStateFromSession(sessionData: SessionMetadataResult) {
    Log.v(TAG, "updateLocalStateFromSession()")
    store.update {
      it.copy(
        sessionId = sessionData.sessionId,
        challengesRequested = sessionData.challengesRequested,
        allowedToRequestCode = sessionData.allowedToRequestCode
      )
    }
  }

  private suspend fun getOrCreateValidSession(context: Context): SessionMetadataResult? {
    Log.v(TAG, "getOrCreateValidSession()")
    val e164 = number.e164Number
    val mccMncProducer = MccMncProducer(context)
    val existingSessionId = store.value.sessionId
    return RegistrationViewModel.getOrCreateValidSession(context = context, existingSessionId = existingSessionId, e164 = e164, password = password, mcc = mccMncProducer.mcc, mnc = mccMncProducer.mnc, successListener = { freshMetadata ->
      Log.v(TAG, "Valid session received, updating local state.")
      updateLocalStateFromSession(freshMetadata)
    }, errorHandler = { result ->
      val requestCode: VerificationCodeRequestResult = when (result) {
        is RegistrationSessionCreationResult.RateLimited -> VerificationCodeRequestResult.RateLimited(result.getCause(), result.timeRemaining)
        is RegistrationSessionCreationResult.MalformedRequest -> VerificationCodeRequestResult.MalformedRequest(result.getCause())
        else -> VerificationCodeRequestResult.UnknownError(result.getCause())
      }

      store.update {
        it.copy(changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(requestCode))
      }
    })
  }

  private suspend fun changeNumberWithRecoveryPassword(): ChangeLocalNumberOutcome {
    Log.v(TAG, "changeNumberWithRecoveryPassword()")
    SignalStore.svr.recoveryPassword?.let { recoveryPassword ->
      val result = repository.changeNumberWithRecoveryPassword(recoveryPassword = recoveryPassword, newE164 = number.e164Number)

      if (result is ChangeNumberResult.Success) {
        return handleSuccessfulChangedRemoteNumber(e164 = result.numberChangeResult.number, pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni), changeNumberOutcome = ChangeNumberOutcome.Succeeded)
      } else if (result is ChangeNumberResult.UnknownError) {
        store.update {
          it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(VerificationCodeRequestResult.UnknownError(result.getCause())))
        }
        return ChangeLocalNumberOutcome.Failure
      }

      Log.d(TAG, "Encountered error while trying to change number with recovery password.", result.getCause())
    }
    return ChangeLocalNumberOutcome.NotPerformed
  }

  private fun handleSuccessfulChangedRemoteNumber(e164: String, pni: ServiceId.PNI, changeNumberOutcome: ChangeNumberOutcome): ChangeLocalNumberOutcome {
    var result = changeNumberOutcome

    Log.v(TAG, "handleSuccessfulChangedRemoteNumber(${result.javaClass.simpleName})")
    val changeLocalNumberResult: ChangeLocalNumberOutcome = try {
      repository.changeLocalNumber(e164, pni)
      SignalStore.misc.clearPendingChangeNumberMetadata()
      ChangeLocalNumberOutcome.Success
    } catch (ioException: IOException) {
      Log.w(TAG, "Failed to change local number!", ioException)
      result = ChangeNumberOutcome.ChangeNumberRequestOutcome(VerificationCodeRequestResult.UnknownError(ioException))
      ChangeLocalNumberOutcome.Failure
    }

    store.update {
      it.copy(inProgress = false, changeNumberOutcome = result)
    }

    return changeLocalNumberResult
  }

  private suspend fun handleVerificationError(result: VerificationCodeRequestResult, verificationErrorHandler: (VerificationCodeRequestResult) -> Unit) {
    Log.v(TAG, "handleVerificationError(${result.javaClass.simpleName}")
    when (result) {
      is VerificationCodeRequestResult.Success -> Unit
      is VerificationCodeRequestResult.RegistrationLocked ->
        store.update {
          it.copy(
            lockedTimeRemaining = result.timeRemaining,
            svr2Credentials = result.svr2Credentials,
            svr3Credentials = result.svr3Credentials
          )
        }

      else -> Log.i(TAG, "Received exception during verification.", result.getCause())
    }

    withContext(Dispatchers.Main) {
      verificationErrorHandler(result)
    }
  }

  private suspend fun handleChangeNumberError(result: ChangeNumberResult, numberChangeErrorHandler: (ChangeNumberResult) -> Unit) {
    Log.v(TAG, "handleChangeNumberError(${result.javaClass.simpleName})")
    when (result) {
      is ChangeNumberResult.Success -> Unit
      is ChangeNumberResult.RegistrationLocked ->
        store.update {
          it.copy(
            svr2Credentials = result.svr2Credentials,
            svr3Credentials = result.svr3Credentials
          )
        }

      is ChangeNumberResult.SvrWrongPin -> {
        store.update {
          it.copy(
            svrTriesRemaining = result.triesRemaining
          )
        }
      }

      else -> Log.i(TAG, "Received exception during change number.", result.getCause())
    }

    withContext(Dispatchers.Main) {
      numberChangeErrorHandler(result)
    }
  }

  private suspend fun requestVerificationCode(context: Context, mode: RegistrationRepository.E164VerificationMode) {
    Log.v(TAG, "requestVerificationCode()")
    val e164 = number.e164Number

    val validSession = getOrCreateValidSession(context)

    if (validSession == null) {
      Log.w(TAG, "Bailing on requesting verification code because could not create a session!")
      resetLocalSessionState()
      return
    }

    if (validSession.verified) {
      Log.i(TAG, "Existing session is already verified; completing the change with the verified session instead of requesting a new code.")
      changeNumberWithVerifiedSession(validSession.sessionId)
      return
    }

    val result = if (!validSession.allowedToRequestCode) {
      val challenges = validSession.challengesRequested.joinToString()
      Log.i(TAG, "Not allowed to request code! Remaining challenges: $challenges")
      VerificationCodeRequestResult.ChallengeRequired(validSession.challengesRequested)
    } else {
      store.update {
        it.copy(changeNumberOutcome = null, challengesRequested = emptyList())
      }
      val response = RegistrationRepository.requestSmsCode(context = context, sessionId = validSession.sessionId, e164 = e164, password = password, mode = mode)
      Log.d(TAG, "SMS code request submitted")
      response
    }

    val challengesRequested = if (result is VerificationCodeRequestResult.ChallengeRequired) {
      result.challenges
    } else {
      emptyList()
    }

    if (result is VerificationCodeRequestResult.AlreadyVerified) {
      Log.i(TAG, "Session became verified while requesting a code; completing the change with the verified session.")
      changeNumberWithVerifiedSession(validSession.sessionId)
      return
    }

    Log.d(TAG, "Received result: ${result.javaClass.canonicalName}\nwith challenges: ${challengesRequested.joinToString { it.key }}")

    val (nextSmsTimestamp, nextCallTimestamp) = when (result) {
      is VerificationCodeRequestResult.Success -> result.nextSmsTimestamp.inWholeMilliseconds to result.nextCallTimestamp.inWholeMilliseconds
      is VerificationCodeRequestResult.RequestVerificationCodeRateLimited -> result.nextSmsTimestamp.inWholeMilliseconds to result.nextCallTimestamp.inWholeMilliseconds
      else -> store.value.nextSmsTimestamp to store.value.nextCallTimestamp
    }

    store.update {
      it.copy(
        changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(result),
        challengesRequested = challengesRequested,
        inProgress = false,
        nextSmsTimestamp = nextSmsTimestamp,
        nextCallTimestamp = nextCallTimestamp
      )
    }
  }

  /**
   * Completes a number change using a registration session that the service already considers verified.
   */
  private suspend fun changeNumberWithVerifiedSession(sessionId: String) {
    Log.v(TAG, "changeNumberWithVerifiedSession()")
    when (val result = repository.changeNumberWithoutRegistrationLock(sessionId = sessionId, newE164 = number.e164Number)) {
      is ChangeNumberResult.Success -> {
        handleSuccessfulChangedRemoteNumber(
          e164 = result.numberChangeResult.number,
          pni = ServiceId.PNI.parseOrThrow(result.numberChangeResult.pni),
          changeNumberOutcome = ChangeNumberOutcome.Succeeded
        )
      }

      is ChangeNumberResult.RegistrationLocked -> {
        if (result.svr2Credentials != null) {
          Log.i(TAG, "Destination number is registration locked; prompting for PIN.")
          store.update {
            it.copy(
              inProgress = false,
              svr2Credentials = result.svr2Credentials,
              svr3Credentials = result.svr3Credentials,
              lockedTimeRemaining = result.timeRemaining,
              changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(
                VerificationCodeRequestResult.RegistrationLocked(result.getCause(), result.timeRemaining, result.svr2Credentials, result.svr3Credentials)
              )
            )
          }
        } else {
          Log.w(TAG, "Destination number is registration locked but SVR credentials were missing, cannot prompt for PIN.")
          store.update {
            it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(VerificationCodeRequestResult.UnknownError(result.getCause())))
          }
        }
      }

      else -> {
        Log.w(TAG, "Unable to complete change number with verified session.", result.getCause())
        store.update {
          it.copy(inProgress = false, changeNumberOutcome = ChangeNumberOutcome.ChangeNumberRequestOutcome(VerificationCodeRequestResult.UnknownError(result.getCause())))
        }
      }
    }
  }

  private suspend fun getRegistrationData(context: Context): RegistrationData {
    val currentState = store.value
    val code = currentState.enteredCode ?: throw IllegalStateException("Can't construct registration data without entered code!")
    val e164: String = number.e164Number ?: throw IllegalStateException("Can't construct registration data without E164!")
    val recoveryPassword = if (currentState.sessionId == null) SignalStore.svr.recoveryPassword else null
    val fcmToken = RegistrationRepository.getFcmToken(context)
    return RegistrationData(code, e164, password, RegistrationRepository.getRegistrationId(), RegistrationRepository.getProfileKey(e164), fcmToken, RegistrationRepository.getPniRegistrationId(), recoveryPassword)
  }

  // endregion

  // region Utility Functions

  /**
   * Used for early returns in order to end the in-progress visual state, as well as print a log message explaining what happened.
   *
   * @param logMessage Logging code is wrapped in lambda so that our automated tools detect the various [Log] calls with their accompanying messages.
   */
  private fun bail(logMessage: () -> Unit) {
    logMessage()
    store.update {
      it.copy(inProgress = false)
    }
  }

  // endregion

  enum class ContinueStatus {
    CAN_CONTINUE, INVALID_NUMBER, OLD_NUMBER_DOESNT_MATCH
  }
}
