app/models/mpi_data.rb
# frozen_string_literal: true

require 'mpi/responses/find_profile_response'
require 'mpi/service'
require 'common/models/redis_store'
require 'common/models/concerns/cache_aside'
require 'mpi/constants'
require 'sentry_logging'

# Facade for MVI. User model delegates MVI correlation id and VA profile (golden record) methods to this class.
# When a profile is requested from one of the delegates it is returned from either a cached response in Redis
# or from the MVI SOAP service.
class MPIData < Common::RedisStore
  include Common::CacheAside
  include SentryLogging

  REDIS_CONFIG_KEY = :mpi_profile_response
  redis_config_key REDIS_CONFIG_KEY

  attr_accessor :user_loa3,
                :user_icn,
                :user_first_name,
                :user_last_name,
                :user_birth_date,
                :user_ssn,
                :user_edipi,
                :user_logingov_uuid,
                :user_idme_uuid,
                :user_uuid

  # Creates a new MPIData instance for a user identity.
  #
  # @param user [UserIdentity] the user identity to query MVI for
  # @return [MPIData] an instance of this class
  def self.for_user(user_identity)
    mvi = MPIData.new

    mvi.user_loa3 = user_identity.loa3?
    mvi.user_icn = user_identity.mhv_icn || user_identity.icn
    mvi.user_first_name = user_identity.first_name
    mvi.user_last_name = user_identity.last_name
    mvi.user_birth_date = user_identity.birth_date
    mvi.user_ssn = user_identity.ssn
    mvi.user_edipi = user_identity.edipi
    mvi.user_logingov_uuid = user_identity.logingov_uuid
    mvi.user_idme_uuid = user_identity.idme_uuid
    mvi.user_uuid = user_identity.uuid
    mvi
  end

  # A DOD EDIPI (Electronic Data Interchange Personal Identifier) MVI correlation ID
  # or nil for users < LOA 3
  #
  # @return [String] the edipi correlation id
  delegate :edipi, to: :profile, allow_nil: true

  # @return [Array[String]] multiple edipi correlation ids
  delegate :edipis, to: :profile, allow_nil: true

  # A ICN (Integration Control Number - generated by the Master Patient Index) MVI correlation ID
  # or nil for users < LOA 3
  #
  # @return [String] the icn correlation id
  delegate :icn, to: :profile, allow_nil: true

  # A ICN (Integration Control Number - generated by the Master Patient Index) MVI correlation ID
  # combined with its Assigning Authority ID.  Or nil for users < LOA 3.
  #
  # @return [String] the icn correlation id with its assigning authority id.
  #   For example:  '12345678901234567^NI^200M^USVHA^P'
  #
  delegate :icn_with_aaid, to: :profile, allow_nil: true

  # A MHV (My HealtheVet) MVI correlation id
  # or nil for users < LOA 3
  #
  # @return [String] the mhv correlation id
  delegate :mhv_correlation_id, to: :profile, allow_nil: true

  # A VBA (Veterans Benefits Administration) or participant MVI correlation id.
  #
  # @return [String] the participant id
  delegate :participant_id, to: :profile, allow_nil: true

  # @return [Array[String]] multiple participant ids
  delegate :participant_ids, to: :profile, allow_nil: true

  # A BIRLS (Beneficiary Identification and Records Locator System) MVI correlation id.
  #
  # @return [String] the birls id
  delegate :birls_id, to: :profile, allow_nil: true

  # @return [Array[String]] multiple birls ids
  delegate :birls_ids, to: :profile, allow_nil: true

  # An MHV id.
  #
  # @return [String] the mhv ien id
  delegate :mhv_ien, to: :profile, allow_nil: true

  # @return [Array[String]] multiple mhv ien ids
  delegate :mhv_iens, to: :profile, allow_nil: true

  # A Vet360 Correlation ID
  #
  # @return [String] the Vet360 id
  delegate :vet360_id, to: :profile, allow_nil: true

  # The search token given in the original MVI 1306 response message
  #
  # @return [String] the search token
  delegate :search_token, to: :profile, allow_nil: true

  # A Cerner ID
  #
  # @return [String] the Cerner id
  delegate :cerner_id, to: :profile, allow_nil: true

  # The user's Cerner facility ids
  #
  # @return [Array[String]] the the list of Cerner facility ids
  delegate :cerner_facility_ids, to: :profile, allow_nil: true

  # Identity theft flag
  #
  # @return [Boolean] presence or absence of identity theft flag
  delegate :id_theft_flag, to: :profile, allow_nil: true

  # The person types that the user's profile represents
  #
  # @return [Array[String]] the list of person types
  delegate :person_types, to: :profile, allow_nil: true

  # The user's home phone number
  #
  # @return [String] the home_phone
  delegate :home_phone, to: :profile, allow_nil: true

  # The profile returned from the MVI service. Either returned from cached response in Redis or the MVI service.
  #
  # @return [MPI::Models::MviProfile] patient 'golden record' data from MVI
  def profile
    return nil unless user_loa3

    mvi_response&.profile
  end

  # The status of the last MVI response or not authorized for for users < LOA 3
  #
  # @return [String] the status of the last MVI response
  def status
    return :not_authorized unless user_loa3

    mvi_response&.status
  end

  # The error experienced when reaching out to the MVI service.
  #
  # @return [Common::Exceptions::BackendServiceException]
  def error
    return Common::Exceptions::Unauthorized.new(source: self.class) unless user_loa3

    mvi_response.try(:error)
  end

  # @return [MPI::Responses::FindProfileResponse] the response returned from MVI
  def mvi_response(user_key: get_user_key)
    @mvi_response ||= response_from_redis_or_service(user_key:)
  end

  def mpi_response_is_cached?(user_key: get_user_key)
    cached?(key: user_key)
  end

  # The status of the MPI Add Person Proxy Add call.
  # An Orchestrated MVI Search needs to be made to obtain a token before an MPI add person proxy add call is made.
  # The cached response is deleted after the response if the call is successful.
  #
  # @return [MPI::Responses::AddPersonResponse] the response returned from MPI
  def add_person_proxy(as_agent: false)
    search_response = mpi_service.find_profile_by_identifier(identifier: user_icn,
                                                             identifier_type: MPI::Constants::ICN)
    if search_response.ok?
      orch_search_response = perform_orchestrated_search(search_response)
      if orch_search_response.ok?
        @mvi_response = orch_search_response
        add_person_response = perform_add_person_proxy(orch_search_response, as_agent:)
        add_ids(add_person_response) if add_person_response.ok?
        add_person_response
      else
        orch_search_response
      end
    else
      search_response
    end
  end

  private

  def perform_orchestrated_search(search_response)
    mpi_service.find_profile_by_attributes_with_orch_search(
      first_name: search_response.profile.given_names.first,
      last_name: search_response.profile.family_name,
      birth_date: search_response.profile.birth_date,
      ssn: search_response.profile.ssn,
      edipi: search_response.profile.edipi
    )
  end

  def perform_add_person_proxy(orch_search_response, as_agent:)
    mpi_service.add_person_proxy(last_name: orch_search_response.profile.family_name,
                                 ssn: orch_search_response.profile.ssn,
                                 birth_date: orch_search_response.profile.birth_date,
                                 icn: orch_search_response.profile.icn,
                                 edipi: orch_search_response.profile.edipi,
                                 search_token: orch_search_response.profile.search_token,
                                 first_name: orch_search_response.profile.given_names.first,
                                 as_agent:)
  end

  def get_user_key
    if user_icn.present?
      user_icn
    elsif user_edipi.present?
      user_edipi
    elsif user_logingov_uuid.present?
      user_logingov_uuid
    elsif user_idme_uuid.present?
      user_idme_uuid
    else
      user_uuid
    end
  end

  def find_profile
    if user_icn.present?
      mpi_service.find_profile_by_identifier(identifier: user_icn, identifier_type: MPI::Constants::ICN)
    elsif user_edipi.present?
      mpi_service.find_profile_by_edipi(edipi: user_edipi)
    elsif user_logingov_uuid.present?
      mpi_service.find_profile_by_identifier(identifier: user_logingov_uuid,
                                             identifier_type: MPI::Constants::LOGINGOV_UUID)
    elsif user_idme_uuid.present?
      mpi_service.find_profile_by_identifier(identifier: user_idme_uuid, identifier_type: MPI::Constants::IDME_UUID)
    else
      mpi_service.find_profile_by_attributes(first_name: user_first_name,
                                             last_name: user_last_name,
                                             birth_date: user_birth_date,
                                             ssn: user_ssn)
    end
  end

  def add_ids(response)
    profile.birls_id = response.parsed_codes[:birls_id].presence
    profile.participant_id = response.parsed_codes[:participant_id].presence

    delete_cached_response if mvi_response.cache?
  end

  def response_from_redis_or_service(user_key:)
    do_cached_with(key: user_key) do
      find_profile
    rescue ArgumentError, MPI::Errors::ArgumentError => e
      log_message_to_sentry("[MPI Data] Request error: #{e.message}", :warn)
      return nil
    end
  end

  def delete_cached_response
    self.class.delete(get_user_key)
    @mvi_response = nil
  end

  def mpi_service
    @service ||= MPI::Service.new
  end

  def save
    saved = super
    expire(record_ttl) if saved
    saved
  end

  def record_ttl
    if status == :ok
      # ensure default ttl is used for 'ok' responses
      REDIS_CONFIG[REDIS_CONFIG_KEY][:each_ttl]
    else
      # assign separate ttl to redis cache for failure responses
      REDIS_CONFIG[REDIS_CONFIG_KEY][:failure_ttl]
    end
  end
end
