# frozen_string_literal: true

module Spree
  class ShippingMethod < ApplicationRecord
    include CalculatedAdjustments

    DISPLAY_ON_OPTIONS = {
      both: "",
      back_end: "back_end"
    }.freeze

    acts_as_paranoid
    acts_as_taggable

    default_scope -> { where(deleted_at: nil) }

    has_many :shipping_rates, inverse_of: :shipping_method, dependent: :destroy
    has_many :shipments, through: :shipping_rates
    has_many :shipping_method_categories, dependent: :destroy
    has_many :shipping_categories, through: :shipping_method_categories
    has_many :distributor_shipping_methods, dependent: :destroy
    has_many :distributors, through: :distributor_shipping_methods,
                            class_name: 'Enterprise',
                            foreign_key: 'distributor_id'

    has_and_belongs_to_many :zones, join_table: 'spree_shipping_methods_zones',
                                    class_name: 'Spree::Zone'

    belongs_to :tax_category, class_name: 'Spree::TaxCategory', optional: true

    validates :name, presence: true
    validate :distributor_validation
    validate :at_least_one_shipping_category
    validates :display_on, inclusion: { in: DISPLAY_ON_OPTIONS.values }, allow_nil: true

    after_initialize :init

    after_save :touch_distributors

    scope :managed_by, lambda { |user|
      if user.admin?
        where(nil)
      else
        joins(:distributors).
          where(distributors_shipping_methods: { distributor_id: user.enterprises.select(&:id) }).
          select('DISTINCT spree_shipping_methods.*')
      end
    }

    scope :for_distributors, ->(distributors) {
      non_unique_matches = unscoped.joins(:distributors).where(enterprises: { id: distributors })
      where(id: non_unique_matches.map(&:id))
    }
    scope :for_distributor, lambda { |distributor|
      joins(:distributors).
        where(enterprises: { id: distributor })
    }

    scope :by_name, -> { order('spree_shipping_methods.name ASC') }

    # Here we allow checkout with shipping methods without zones (see issue #3928 for details)
    #   and also checkout with addresses outside of the zones of the selected shipping method
    # This method could be used, like in Spree, to validate shipping method zones on checkout.
    def delivers_to?(address)
      address.present?
    end

    def build_tracking_url(tracking)
      tracking_url.gsub(":tracking", tracking) unless tracking.blank? || tracking_url.blank?
    end

    # Some shipping methods are only meant to be set via backend
    def frontend?
      display_on != "back_end"
    end

    def has_distributor?(distributor)
      distributors.include?(distributor)
    end

    # Checks whether the shipping method is of delivery type, meaning that it
    # requires the user to specify a ship address at checkout.
    #
    # @return [Boolean]
    def delivery?
      require_ship_address
    end

    # Return the services (pickup, delivery) that different distributors provide, in the format:
    # {distributor_id => {pickup: true, delivery: false}, ...}
    #
    # Optionally, specify some distributor_ids as a parameter to scope the results
    def self.services(distributor_ids = nil)
      methods = Spree::ShippingMethod.joins(:distributor_shipping_methods).group('distributor_id')

      if distributor_ids.present?
        methods = methods.where(distributor_shipping_methods: { distributor_id: distributor_ids })
      end

      methods.
        pluck(Arel.sql("distributor_id"),
              Arel.sql("BOOL_OR(spree_shipping_methods.require_ship_address = 'f') AS pickup"),
              Arel.sql("BOOL_OR(spree_shipping_methods.require_ship_address = 't') AS delivery")).
        to_h { |(distributor_id, pickup, delivery)| [distributor_id.to_i, { pickup:, delivery: }] }
    end

    def self.backend
      where(display_on: DISPLAY_ON_OPTIONS[:back_end])
    end

    def self.frontend
      where(display_on: [nil, ""])
    end

    def init
      self.calculator ||= ::Calculator::None.new if new_record?
    end

    private

    def no_active_or_upcoming_order_cycle_distributors_with_only_one_shipping_method?
      return true if new_record?

      distributors.
        with_order_cycles_as_distributor_outer.
        merge(OrderCycle.active.or(OrderCycle.upcoming)).none? do |distributor|
        distributor.shipping_method_ids.one?
      end
    end

    def at_least_one_shipping_category
      return unless shipping_categories.empty?

      errors.add(:base, "You need to select at least one shipping category")
    end

    def touch_distributors
      distributors.each do |distributor|
        distributor.touch if distributor.persisted?
      end
    end

    def distributor_validation
      validates_with DistributorsValidator
    end
  end
end
