# frozen_string_literal: true

require 'phony'
require 'phony_rails/string_extensions'
require 'validators/phony_validator'
require 'phony_rails/version'
require 'yaml'

module PhonyRails
  def self.default_country_code
    @default_country_code ||= nil
  end

  def self.default_country_code=(new_code)
    @default_country_code = new_code
    @default_country_number = nil # Reset default country number, will lookup next time its asked for
  end

  def self.default_country_number
    @default_country_number ||= default_country_code.present? ? country_number_for(default_country_code) : nil
  end

  def self.default_country_number=(new_number)
    @default_country_number = new_number
  end

  def self.country_number_for(country_code)
    return if country_code.nil?

    country_codes_hash.fetch(country_code.to_s.upcase, {})['country_code']
  end

  def self.country_codes_hash
    @country_codes_hash ||= YAML.load_file(File.join(File.dirname(File.expand_path(__FILE__)), 'data/country_codes.yaml'))
  end

  # This method requires a country_code attribute (eg. NL) and phone_number to be set.
  # Options:
  #   :country_number => The country dial code (eg. 31 for NL).
  #   :default_country_number => Fallback country code.
  #   :country_code => The country code we should use.
  #   :default_country_code => Some fallback code (eg. 'NL') that can be used as default (comes from phony_normalize_numbers method).
  #   :add_plus => Add a '+' in front so we know the country code is added. (default: true)
  #   :extension => Include the extension. (default: true)
  # This idea came from:
  #   http://www.redguava.com.au/2011/06/rails-convert-phone-numbers-to-international-format-for-sms/
  def self.normalize_number(number, options = {}, current_instance = nil)
    return if number.nil?

    original_number = number
    number = number.dup # Just to be sure, we don't want to change the original.
    number, ext = extract_extension(number)
    number.gsub!(/[^()\d+]/, '') # Strips weird stuff from the number
    return if number.blank?

    if _country_number = options[:country_number] || country_number_for(options[:country_code])
      options[:add_plus] = true if options[:add_plus].nil?
      # (Force) add country_number if missing
      # NOTE: do we need to force adding country code? Otherwise we can share logic with next block
      number = "#{_country_number}#{number}" if !Phony.plausible?(number) || _country_number != country_code_from_number(number)
    elsif _default_country_number = extract_default_country_number(options, current_instance)
      options[:add_plus] = true if options[:add_plus].nil?
      number = normalize_number_default_country(number, _default_country_number)
    end
    normalized_number = Phony.normalize(number)
    options[:add_plus] = true if options[:add_plus].nil? && Phony.plausible?(normalized_number)
    normalized_number = options[:add_plus] ? "+#{normalized_number}" : normalized_number

    options[:extension] = true if options[:extension].nil?
    options[:extension] ? format_extension(normalized_number, ext) : normalized_number
  rescue StandardError
    original_number # If all goes wrong .. we still return the original input.
  end

  def self.normalize_number_default_country(number, default_country_number)
    # We try to add the default country number and see if it is a
    # correct phone number. See https://github.com/joost/phony_rails/issues/87#issuecomment-89324426
    unless number =~ /\A\(?(\+|00)/ # if we don't have a + or 00
      return "#{default_country_number}#{number}" if Phony.plausible?("#{default_country_number}#{number}") || !Phony.plausible?(number) || country_code_from_number(number).nil?
      # If the number starts with ONE zero (two might indicate a country code)
      # and this is a plausible number for the default_country
      # we prefer that one.
      return "#{default_country_number}#{number.gsub(/^0/, '')}" if (number =~ /^0[^0]/) && Phony.plausible?("#{default_country_number}#{number.gsub(/^0/, '')}")
    end
    # number = "#{default_country_number}#{number}" unless Phony.plausible?(number)
    # Just return the number unchanged
    number
  end

  def self.extract_default_country_number(options = {}, current_instance = nil)
    country_code = if current_instance.present? && options[:default_country_code].respond_to?(:call)
                     options[:default_country_code].call(current_instance) rescue nil
                   else
                     options[:default_country_code]
                   end
    options[:default_country_number] || country_number_for(country_code) || default_country_number
  end

  # Returns the country dail code (eg. '31') for a number (eg. +31612341234).
  # Should probably be named 'country_number_from_number'.
  def self.country_code_from_number(number)
    return nil unless Phony.plausible?(number)

    Phony.split(Phony.normalize(number)).first
  end

  # Returns the country (eg. 'NL') for a number (eg. +31612341234).
  def self.country_from_number(number)
    return nil unless Phony.plausible?(number)

    country_codes_hash.select { |_country, hash| hash['country_code'] == country_code_from_number(number) }.keys[0]
  end

  # Wrapper for Phony.plausible?.  Takes the same options as #normalize_number.
  # NB: This method calls #normalize_number and passes _options_ directly to that method.
  # It uses the 'cc' option for Phony. This was a required param before?
  def self.plausible_number?(number, options = {})
    return false if number.blank?

    number = extract_extension(number).first
    number = normalize_number(number, options)
    country_number = options[:country_number] || country_number_for(options[:country_code]) ||
                     country_code_from_number(number) ||
                     options[:default_country_number] || country_number_for(options[:default_country_code]) ||
                     default_country_number
    Phony.plausible? number, cc: country_number
  rescue StandardError
    false
  end

  COMMON_EXTENSIONS = / *(ext|ex|x|xt|#|:)+[^0-9]*\(?([-0-9]{1,})\)?#?$/i.freeze

  def self.extract_extension(number_and_ext)
    return [nil, nil] if number_and_ext.nil?

    subbed = number_and_ext.sub(COMMON_EXTENSIONS, '')
    [subbed, Regexp.last_match(2)]
  end

  def self.format_extension(number, ext)
    ext.present? ? "#{number} x#{ext}" : number
  end

  module Extension
    extend ActiveSupport::Concern

    included do
      private

      # This methods sets the attribute to the normalized version.
      # It also adds the country_code (number), eg. 31 for NL numbers.
      def set_phony_normalized_numbers(current_instance, attributes, options = {})
        options = options.dup
        assign_values_for_phony_symbol_options(options)
        if respond_to?(:country_code)
          set_country_as = options[:enforce_record_country] ? :country_code : :default_country_code
          options[set_country_as] ||= country_code
        end
        attributes.each do |attribute|
          attribute_name = options[:as] || attribute
          raise("No attribute #{attribute_name} found on #{self.class.name} (PhonyRails)") unless self.class.attribute_method?(attribute_name)

          cache_original_attribute(current_instance, attribute) if options[:normalize_when_valid]
          new_value = PhonyRails.normalize_number(send(attribute), options, current_instance)
          current_instance.public_send("#{attribute_name}=", new_value) if new_value || attribute_name != attribute
        end
      end

      def assign_values_for_phony_symbol_options(options)
        symbol_options = %i[country_number default_country_number country_code default_country_code]
        symbol_options.each do |option|
          options[option] = send(options[option]) if options[option].is_a?(Symbol)
        end
      end
    end

    def cache_original_attribute(current_instance, attribute)
      attribute_name = "#{attribute}_original"
      current_instance.define_singleton_method("#{attribute_name}=") { |value| instance_variable_set("@#{attribute_name}", value) }
      current_instance.define_singleton_method(attribute_name) { instance_variable_get("@#{attribute_name}") }
      current_instance.public_send("#{attribute}_original=", current_instance.public_send(attribute.to_s))
    end

    module ClassMethods
      PHONY_RAILS_COLLECTION_VALID_KEYS = %i[
        add_plus
        as
        country_code
        country_number
        default_country_code
        default_country_number
        enforce_record_country
        if
        normalize_when_valid
        unless
      ].freeze
      # Use this method on the class level like:
      #   phony_normalize :phone_number, :fax_number, :default_country_code => 'NL'
      #
      # It checks your model object for a a country_code attribute (eg. 'NL') to do the normalizing so make sure
      # you've geocoded before calling this method!
      def phony_normalize(*attributes)
        options = attributes.last.is_a?(Hash) ? attributes.pop : {}
        options.assert_valid_keys(*PHONY_RAILS_COLLECTION_VALID_KEYS)
        raise ArgumentError, ':as option can not be used on phony_normalize with multiple attribute names! (PhonyRails)' if options[:as].present? && (attributes.size > 1)

        options[:enforce_record_country] = true if options[:enforce_record_country].nil?

        conditional = create_before_validation_conditional_hash(options)

        # Add before validation that saves a normalized version of the phone number
        before_validation conditional do
          set_phony_normalized_numbers(self, attributes, options)
        end
      end

      # Usage:
      #   phony_normalized_method :fax_number, :default_country_code => 'US'
      # Creates a normalized_fax_number method.
      def phony_normalized_method(*attributes)
        main_options = attributes.last.is_a?(Hash) ? attributes.pop : {}
        main_options.assert_valid_keys :country_code, :default_country_code
        attributes.each do |attribute|
          raise(StandardError, "Instance method normalized_#{attribute} already exists on #{name} (PhonyRails)") if method_defined?(:"normalized_#{attribute}")

          define_method :"normalized_#{attribute}" do |*args|
            options = main_options.merge(args.first || {})
            assign_values_for_phony_symbol_options(options)
            raise(ArgumentError, "No attribute/method #{attribute} found on #{self.class.name} (PhonyRails)") unless respond_to?(attribute)

            options[:country_code] ||= country_code if respond_to?(:country_code)
            PhonyRails.normalize_number(send(attribute), options)
          end
        end
      end

      private

      # Creates a hash representing a conditional for before_validation
      # This allows conditional normalization
      # Returns something like `{ unless: -> { attribute == 'something' } }`
      # If no if/unless options passed in, returns `{ if: -> { true } }`
      def create_before_validation_conditional_hash(options)
        if options[:if].present?
          type = :if
          source = options[:if]
        elsif options[:unless].present?
          type = :unless
          source = options[:unless]
        else
          type = :if
          source = true
        end

        conditional = {}
        conditional[type] = if source.respond_to?(:call)
                              source
                            elsif source.respond_to?(:to_sym)
                              -> { send(source.to_sym) }
                            else
                              -> { source }
                            end
        conditional
      end
    end
  end
end

# check whether it is ActiveRecord or Mongoid being used
if defined?(ActiveRecord)
  ActiveSupport.on_load(:active_record) do
    ActiveRecord::Base.send :include, PhonyRails::Extension
  end
end

ActiveModel::Model.send :include, PhonyRails::Extension if defined?(ActiveModel::Model)

# if defined?(Mongoid)
#   module Mongoid::Phony
#     extend ActiveSupport::Concern
#     include PhonyRails::Extension
#   end
# end

Dir["#{File.dirname(__FILE__)}/phony_rails/locales/*.yml"].each do |file|
  I18n.load_path << file
end
