# frozen_string_literal: true

module Spree
  class Zone < ApplicationRecord
    has_many :zone_members, dependent: :destroy, class_name: "Spree::ZoneMember", inverse_of: :zone
    has_many :tax_rates, dependent: :destroy, inverse_of: :zone
    has_and_belongs_to_many :shipping_methods, join_table: 'spree_shipping_methods_zones'

    validates :name, presence: true, uniqueness: true
    validates :zone_members, presence: true

    after_save :remove_defunct_members
    after_save :remove_previous_default

    alias :members :zone_members
    accepts_nested_attributes_for :zone_members, allow_destroy: true,
                                                 reject_if: proc { |a| a['zoneable_id'].blank? }

    def kind
      return unless members.any? && members.none? { |member| member.try(:zoneable_type).nil? }

      members.last.zoneable_type.demodulize.underscore
    end

    def kind=(value)
      # do nothing - just here to satisfy the form
    end

    def contains_address?(address)
      return false unless address

      members.any? do |zone_member|
        case zone_member.zoneable_type
        when 'Spree::Country'
          zone_member.zoneable_id == address.country_id
        when 'Spree::State'
          zone_member.zoneable_id == address.state_id
        else
          false
        end
      end
    end

    # Returns the matching zone with the highest priority zone type (State, Country, Zone.)
    # Returns nil in the case of no matches.
    def self.match(address)
      return unless matches = includes(:zone_members).
        order('zone_members_count', 'created_at').
        select { |zone| zone.contains_address? address }

      ['state', 'country'].each do |zone_kind|
        if match = matches.detect { |zone| zone_kind == zone.kind }
          return match
        end
      end
      matches.first
    end

    # convenience method for returning the countries contained within a zone
    def countries
      @countries ||= case kind
                     when 'country' then zoneables
                     when 'state' then zoneables.collect(&:country)
                     end.flatten.compact.uniq
    end

    def <=>(other)
      name <=> other.name
    end

    # All zoneables belonging to the zone members.  Will be a collection of either
    # countries or states depending on the zone type.
    def zoneables
      members.collect(&:zoneable)
    end

    def country_ids
      if kind == 'country'
        members.collect(&:zoneable_id)
      else
        []
      end
    end

    def state_ids
      if kind == 'state'
        members.collect(&:zoneable_id)
      else
        []
      end
    end

    def country_ids=(ids)
      zone_members.destroy_all
      ids.compact_blank.map do |id|
        member = ZoneMember.new
        member.zoneable_type = 'Spree::Country'
        member.zoneable_id = id
        members << member
      end
    end

    def state_ids=(ids)
      zone_members.destroy_all
      ids.compact_blank.map do |id|
        member = ZoneMember.new
        member.zoneable_type = 'Spree::State'
        member.zoneable_id = id
        members << member
      end
    end

    def self.default_tax
      find_by(default_tax: true)
    end

    # Indicates whether the specified zone falls entirely within the zone performing
    # the check.
    def contains?(target)
      return false if kind == 'state' && target.kind == 'country'
      return false if zone_members.empty? || target.zone_members.empty?

      if kind == target.kind
        if target.zoneables.any? { |target_zoneable| zoneables.exclude?(target_zoneable) }
          return false
        end
      elsif target.zoneables.any? { |target_state| zoneables.exclude?(target_state.country) }
        return false
      end
      true
    end

    private

    def remove_defunct_members
      return unless zone_members.any?

      zone_members.where('zoneable_id IS NULL OR zoneable_type != ?',
                         "Spree::#{kind.capitalize}").destroy_all
    end

    def remove_previous_default
      Spree::Zone.where.not(id:).update_all(default_tax: false) if default_tax
    end
  end
end
