# frozen_string_literal: true

require 'open_food_network/permissions'
require 'open_food_network/order_cycle_form_applicator'

module OrderCycles
  class FormService
    def initialize(order_cycle, order_cycle_params, user)
      @order_cycle = order_cycle
      @confirm_datetime_change = order_cycle_params.delete :confirm_datetime_change
      @error_class = order_cycle_params.delete :error_class
      @order_cycle_params = order_cycle_params
      @specified_params = order_cycle_params.keys
      @user = user
      @permissions = OpenFoodNetwork::Permissions.new(user)
      @schedule_ids = order_cycle_params.delete(:schedule_ids)
      @selected_distributor_payment_method_ids = order_cycle_params.delete(
        :selected_distributor_payment_method_ids
      )
      @selected_distributor_shipping_method_ids = order_cycle_params.delete(
        :selected_distributor_shipping_method_ids
      )
    end

    def save
      # Check that order cycle datetime values changed if it has existing orders
      verify_datetime_change!

      schedule_ids = build_schedule_ids
      order_cycle.assign_attributes(order_cycle_params)
      return false unless order_cycle.valid?

      order_cycle.transaction do
        order_cycle.save!
        order_cycle.schedule_ids = schedule_ids if parameter_specified?(:schedule_ids)
        order_cycle.save!
        apply_exchange_changes
        if can_update_selected_payment_or_shipping_methods?
          attach_selected_distributor_payment_methods
          attach_selected_distributor_shipping_methods
        end
        sync_subscriptions
        true
      end
    rescue ActiveRecord::RecordInvalid => e
      add_exception_to_order_cycle_errors(e)
      false
    end

    private

    attr_accessor :order_cycle, :order_cycle_params, :user, :permissions

    def add_exception_to_order_cycle_errors(exception)
      error = exception.message.split(":").last.strip
      order_cycle.errors.add(:base, error) if order_cycle.errors.to_a.exclude?(error)
    end

    def apply_exchange_changes
      return if exchanges_unchanged?

      OpenFoodNetwork::OrderCycleFormApplicator.new(order_cycle, user).go!

      # reload so outgoing exchanges are up-to-date for shipping/payment method validations
      order_cycle.reload
    end

    def attach_selected_distributor_payment_methods
      return if @selected_distributor_payment_method_ids.nil?

      if distributor_only?
        payment_method_ids = order_cycle.selected_distributor_payment_method_ids
        payment_method_ids -= user_distributor_payment_method_ids
        payment_method_ids += user_only_selected_distributor_payment_method_ids
        order_cycle.selected_distributor_payment_method_ids = payment_method_ids
      else
        order_cycle
          .selected_distributor_payment_method_ids = selected_distributor_payment_method_ids
      end
      order_cycle.save!
    end

    def attach_selected_distributor_shipping_methods
      return if @selected_distributor_shipping_method_ids.nil?

      if distributor_only?
        # A distributor can only update methods associated with their own
        # enterprise, so we load all previously selected methods, and replace
        # only the distributor's methods with their selection (not touching other
        # distributor's methods).
        shipping_method_ids = order_cycle.selected_distributor_shipping_method_ids
        shipping_method_ids -= user_distributor_shipping_method_ids
        shipping_method_ids += user_only_selected_distributor_shipping_method_ids
        order_cycle.selected_distributor_shipping_method_ids = shipping_method_ids
      else
        order_cycle.selected_distributor_shipping_method_ids =
          selected_distributor_shipping_method_ids
      end

      order_cycle.save!
    end

    def attachable_distributor_payment_method_ids
      @attachable_distributor_payment_method_ids ||=
        order_cycle.attachable_distributor_payment_methods.map(&:id)
    end

    def attachable_distributor_shipping_method_ids
      @attachable_distributor_shipping_method_ids ||=
        order_cycle.attachable_distributor_shipping_methods.map(&:id)
    end

    def exchanges_unchanged?
      [:incoming_exchanges, :outgoing_exchanges].all? do |direction|
        order_cycle_params[direction].nil?
      end
    end

    def selected_distributor_payment_method_ids
      @selected_distributor_payment_method_ids = (
        attachable_distributor_payment_method_ids &
        @selected_distributor_payment_method_ids.compact_blank.map(&:to_i)
      )

      if attachable_distributor_payment_method_ids.sort ==
         @selected_distributor_payment_method_ids.sort
        @selected_distributor_payment_method_ids = []
      end

      @selected_distributor_payment_method_ids
    end

    def user_only_selected_distributor_payment_method_ids
      user_distributor_payment_method_ids.intersection(selected_distributor_payment_method_ids)
    end

    def selected_distributor_shipping_method_ids
      @selected_distributor_shipping_method_ids = (
        attachable_distributor_shipping_method_ids &
        @selected_distributor_shipping_method_ids.compact_blank.map(&:to_i)
      )

      if attachable_distributor_shipping_method_ids.sort ==
         @selected_distributor_shipping_method_ids.sort
        @selected_distributor_shipping_method_ids = []
      end

      @selected_distributor_shipping_method_ids
    end

    def user_only_selected_distributor_shipping_method_ids
      user_distributor_shipping_method_ids.intersection(selected_distributor_shipping_method_ids)
    end

    def build_schedule_ids
      return unless parameter_specified?(:schedule_ids)

      result = existing_schedule_ids
      result |= (requested_schedule_ids & permitted_schedule_ids) # Add permitted and requested
      # Remove permitted but not requested
      result -= ((result & permitted_schedule_ids) - requested_schedule_ids)
      result
    end

    def sync_subscriptions
      return unless parameter_specified?(:schedule_ids)
      return unless schedule_sync_required?

      OrderManagement::Subscriptions::ProxyOrderSyncer.new(subscriptions_to_sync).sync!
    end

    def schedule_sync_required?
      removed_schedule_ids.any? || new_schedule_ids.any?
    end

    def subscriptions_to_sync
      Subscription.where(schedule_id: removed_schedule_ids + new_schedule_ids)
    end

    def requested_schedule_ids
      @schedule_ids.map(&:to_i)
    end

    def parameter_specified?(key)
      @specified_params.map(&:to_s).include?(key.to_s)
    end

    def permitted_schedule_ids
      Schedule.where(id: requested_schedule_ids | existing_schedule_ids)
        .merge(permissions.editable_schedules).pluck(:id)
    end

    def existing_schedule_ids
      @existing_schedule_ids ||= order_cycle.persisted? ? order_cycle.schedule_ids : []
    end

    def removed_schedule_ids
      existing_schedule_ids - order_cycle.schedule_ids
    end

    def new_schedule_ids
      @order_cycle.schedule_ids - existing_schedule_ids
    end

    def can_update_selected_payment_or_shipping_methods?
      @user.admin? || coordinator? || distributor?
    end

    def coordinator?
      @user.enterprises.include?(@order_cycle.coordinator)
    end

    def distributor?
      !user_distributors_ids.empty?
    end

    def distributor_only?
      distributor? && !@user.admin? && !coordinator?
    end

    def user_distributors_ids
      @user_distributors_ids ||= @user.enterprises.pluck(:id)
        .intersection(@order_cycle.distributors.pluck(:id))
    end

    def user_distributor_payment_method_ids
      @user_distributor_payment_method_ids ||=
        DistributorPaymentMethod.where(distributor_id: user_distributors_ids)
          .pluck(:id)
    end

    def user_distributor_shipping_method_ids
      @user_distributor_shipping_method_ids ||=
        DistributorShippingMethod.where(distributor_id: user_distributors_ids)
          .pluck(:id)
    end

    def verify_datetime_change!
      return unless @confirm_datetime_change
      return unless @order_cycle.orders.exists?
      return if @order_cycle.same_datetime_value(:orders_open_at,
                                                 @order_cycle_params[:orders_open_at]) &&
                @order_cycle.same_datetime_value(:orders_close_at,
                                                 @order_cycle_params[:orders_close_at])

      raise @error_class
    end
  end
end
