class Twilio::IncomingMessageService
  include ::FileTypeHelper

  pattr_initialize [:params!]

  def perform
    return if twilio_channel.blank?

    set_contact
    set_conversation
    @message = @conversation.messages.build(
      content: message_body,
      account_id: @inbox.account_id,
      inbox_id: @inbox.id,
      message_type: :incoming,
      sender: @contact,
      source_id: params[:SmsSid]
    )
    attach_files
    attach_location if location_message?
    @message.save!
  end

  private

  def twilio_channel
    @twilio_channel ||= ::Channel::TwilioSms.find_by(messaging_service_sid: params[:MessagingServiceSid]) if params[:MessagingServiceSid].present?
    if params[:AccountSid].present? && params[:To].present?
      @twilio_channel ||= ::Channel::TwilioSms.find_by(account_sid: params[:AccountSid],
                                                       phone_number: params[:To])
    end
    log_channel_not_found if @twilio_channel.blank?
    @twilio_channel
  end

  def log_channel_not_found
    Rails.logger.warn(
      '[TWILIO] Incoming message channel lookup failed ' \
      "account_sid=#{params[:AccountSid]} " \
      "to=#{params[:To]} " \
      "messaging_service_sid=#{params[:MessagingServiceSid]} " \
      "sms_sid=#{params[:SmsSid]}"
    )
  end

  def inbox
    @inbox ||= twilio_channel.inbox
  end

  def account
    @account ||= inbox.account
  end

  def phone_number
    twilio_channel.sms? ? params[:From] : params[:From].gsub('whatsapp:', '')
  end

  def normalized_phone_number
    return phone_number unless twilio_channel.whatsapp?

    Whatsapp::PhoneNumberNormalizationService.new(inbox).normalize_and_find_contact_by_provider("whatsapp:#{phone_number}", :twilio)
  end

  def formatted_phone_number
    TelephoneNumber.parse(phone_number).international_number
  end

  def message_body
    params[:Body]&.delete("\u0000")
  end

  def set_contact
    source_id = twilio_channel.whatsapp? ? normalized_phone_number : params[:From]

    contact_inbox = ::ContactInboxWithContactBuilder.new(
      source_id: source_id,
      inbox: inbox,
      contact_attributes: contact_attributes
    ).perform

    @contact_inbox = contact_inbox
    @contact = contact_inbox.contact

    # Update existing contact name if ProfileName is available and current name is just phone number
    update_contact_name_if_needed
  end

  def conversation_params
    {
      account_id: @inbox.account_id,
      inbox_id: @inbox.id,
      contact_id: @contact.id,
      contact_inbox_id: @contact_inbox.id,
      additional_attributes: additional_attributes
    }
  end

  def set_conversation
    # if lock to single conversation is disabled, we will create a new conversation if previous conversation is resolved
    @conversation = if @inbox.lock_to_single_conversation
                      @contact_inbox.conversations.last
                    else
                      @contact_inbox.conversations.where
                                    .not(status: :resolved).last
                    end
    return if @conversation

    @conversation = ::Conversation.create!(conversation_params)
  end

  def contact_attributes
    {
      name: contact_name,
      phone_number: phone_number,
      additional_attributes: additional_attributes
    }
  end

  def contact_name
    params[:ProfileName].presence || formatted_phone_number
  end

  def additional_attributes
    if twilio_channel.sms?
      {
        from_zip_code: params[:FromZip],
        from_country: params[:FromCountry],
        from_state: params[:FromState]
      }
    else
      {}
    end
  end

  def attach_files
    num_media = params[:NumMedia].to_i
    return if num_media.zero?

    num_media.times do |i|
      media_url = params[:"MediaUrl#{i}"]
      attach_single_file(media_url) if media_url.present?
    end
  end

  def attach_single_file(media_url)
    attachment_file = download_attachment_file(media_url)
    return if attachment_file.blank?

    @message.attachments.new(
      account_id: @message.account_id,
      file_type: file_type(attachment_file.content_type),
      file: {
        io: attachment_file,
        filename: attachment_file.original_filename,
        content_type: attachment_file.content_type
      }
    )
  end

  def download_attachment_file(media_url)
    download_with_auth(media_url)
  rescue Down::Error, Down::ClientError => e
    handle_download_attachment_error(e, media_url)
  end

  def download_with_auth(media_url)
    auth_credentials = if twilio_channel.api_key_sid.present?
                         # When using api_key_sid, the auth token should be the api_secret_key
                         [twilio_channel.api_key_sid, twilio_channel.auth_token]
                       else
                         # When using account_sid, the auth token is the account's auth token
                         [twilio_channel.account_sid, twilio_channel.auth_token]
                       end

    Down.download(media_url, http_basic_authentication: auth_credentials)
  end

  def handle_download_attachment_error(error, media_url)
    Rails.logger.info "Error downloading attachment from Twilio: #{error.message}: Retrying without auth"
    Down.download(media_url)
  rescue StandardError => e
    Rails.logger.info "Error downloading attachment from Twilio: #{e.message}: Skipping"
    nil
  end

  def location_message?
    params[:MessageType] == 'location' && params[:Latitude].present? && params[:Longitude].present?
  end

  def attach_location
    @message.attachments.new(
      account_id: @message.account_id,
      file_type: :location,
      coordinates_lat: params[:Latitude].to_f,
      coordinates_long: params[:Longitude].to_f
    )
  end

  def update_contact_name_if_needed
    return if params[:ProfileName].blank?
    return if @contact.name == params[:ProfileName]

    # Only update if current name exactly matches the phone number or formatted phone number
    return unless contact_name_matches_phone_number?

    @contact.update!(name: params[:ProfileName])
  end

  def contact_name_matches_phone_number?
    @contact.name == phone_number || @contact.name == formatted_phone_number
  end
end
