module Spree
  class TaxRate < Spree::Base
    acts_as_paranoid

    include Spree::CalculatedAdjustments
    include Spree::AdjustmentSource

    with_options inverse_of: :tax_rates do
      belongs_to :zone, class_name: 'Spree::Zone', optional: true
      belongs_to :tax_category,
                 class_name: 'Spree::TaxCategory'
    end

    with_options presence: true do
      validates :amount, numericality: { allow_nil: true }
      validates :tax_category
    end

    scope :by_zone, ->(zone) { where(zone_id: zone.id) }
    scope :potential_rates_for_zone,
          ->(zone) do
            where(zone_id: Spree::Zone.potential_matching_zones(zone).pluck(:id))
          end
    scope :for_default_zone,
          -> { potential_rates_for_zone(Spree::Zone.default_tax) }
    scope :for_tax_category,
          ->(category) { where(tax_category_id: category.try(:id)) }
    scope :included_in_price, -> { where(included_in_price: true) }

    # Gets the array of TaxRates appropriate for the specified tax zone
    def self.match(order_tax_zone)
      return [] unless order_tax_zone

      potential_rates_for_zone(order_tax_zone)
    end

    # Pre-tax amounts must be stored so that we can calculate
    # correct rate amounts in the future. For example:
    # https://github.com/spree/spree/issues/4318#issuecomment-34723428
    def self.store_pre_tax_amount(item, rates)
      pre_tax_amount = case item.class.to_s
                       when 'Spree::LineItem' then item.discounted_amount
                       when 'Spree::Shipment' then item.discounted_cost
                       end

      included_rates = rates.select(&:included_in_price)
      if included_rates.any?
        pre_tax_amount /= (1 + included_rates.sum(&:amount))
      end

      item.update_column(:pre_tax_amount, pre_tax_amount)
    end

    # Deletes all tax adjustments, then applies all applicable rates
    # to relevant items.
    def self.adjust(order, items)
      rates = match(order.tax_zone)
      tax_categories = rates.map(&:tax_category)

      # using destroy_all to ensure adjustment destroy callback fires.
      Spree::Adjustment.where(adjustable: items).tax.destroy_all

      relevant_items = items.select do |item|
        tax_categories.include?(item.tax_category)
      end

      relevant_items.each do |item|
        relevant_rates = rates.select do |rate|
          rate.tax_category == item.tax_category
        end
        store_pre_tax_amount(item, relevant_rates)
        relevant_rates.each do |rate|
          rate.adjust(order, item)
        end
      end

      # updates pre_tax for items without any tax rates
      remaining_items = items - relevant_items
      remaining_items.each do |item|
        store_pre_tax_amount(item, [])
      end
    end

    def self.included_tax_amount_for(options)
      return 0 unless options[:tax_zone] && options[:tax_category]

      potential_rates_for_zone(options[:tax_zone]).
        included_in_price.
        for_tax_category(options[:tax_category]).
        sum(:amount)
    end

    def adjust(order, item)
      create_adjustment(order, item, included_in_price)
    end

    def compute_amount(item)
      compute(item)
    end

    private

    def label
      Spree.t included_in_price? ? :including_tax : :excluding_tax,
              scope: 'adjustment_labels.tax_rates',
              name: name.presence || tax_category.name,
              amount: amount_for_label
    end

    def amount_for_label
      return '' unless show_rate_in_label?

      ' ' + ActiveSupport::NumberHelper::NumberToPercentageConverter.convert(
        amount * 100,
        locale: I18n.locale
      )
    end
  end
end
