# frozen_string_literal: true

require 'open_food_network/scope_variant_to_hub'

module Spree
  class LineItem < ApplicationRecord
    include VariantUnits::VariantAndLineItemNaming

    searchable_attributes :price, :quantity, :order_id, :variant_id, :tax_category_id
    searchable_associations :order, :order_cycle, :variant, :product, :supplier, :tax_category
    searchable_scopes :with_tax, :without_tax

    belongs_to :order, class_name: "Spree::Order", inverse_of: :line_items
    has_one :order_cycle, through: :order

    belongs_to :variant, -> { with_deleted }, class_name: "Spree::Variant", inverse_of: :line_items
    has_one :product, through: :variant
    has_one :supplier, through: :variant
    belongs_to :tax_category, class_name: "Spree::TaxCategory", optional: true

    has_many :adjustments, as: :adjustable, dependent: :destroy

    before_validation :adjust_quantity
    before_validation :copy_price
    before_validation :copy_tax_category
    before_validation :copy_dimensions

    validates :quantity, numericality: {
      only_integer: true,
      greater_than: -1,
      message: Spree.t('validation.must_be_int')
    }
    validates :price, numericality: true
    validates_with Stock::AvailabilityValidator

    before_save :update_inventory
    before_save :calculate_final_weight_volume, if: :quantity_changed?,
                                                unless: :final_weight_volume_changed?
    before_save :assign_units, if: ->(line_item) {
      line_item.new_record? || line_item.final_weight_volume_changed?
    }

    before_destroy :update_inventory_before_destroy

    after_destroy :update_order
    after_save :update_order

    delegate :product, :variant_unit, :unit_description, :display_name, :display_as,
             :variant_unit_scale, :variant_unit_name, to: :variant

    # Allows manual skipping of Stock::AvailabilityValidator
    attr_accessor :skip_stock_check, :target_shipment

    attribute :restock_item, type: :boolean, default: true

    # -- Scopes
    scope :managed_by, lambda { |user|
      if user.admin?
        where(nil)
      else
        # Find line items that are from orders distributed by the user or supplied by the user
        joins(variant: :product).
          joins(:order).
          where('spree_orders.distributor_id IN (?) OR spree_products.supplier_id IN (?)',
                user.enterprises, user.enterprises).
          select('spree_line_items.*')
      end
    }

    scope :in_orders, lambda { |orders|
      where(order_id: orders)
    }

    # Find line items that are from order sorted by variant name and unit value
    scope :sorted_by_name_and_unit_value, -> {
      joins(variant: :product).
        reorder(Arel.sql("
          lower(spree_products.name) asc,
            lower(spree_variants.display_name) asc,
            spree_variants.unit_value asc"))
    }

    scope :from_order_cycle, lambda { |order_cycle|
      joins(order: :order_cycle).
        where(order_cycles: { id: order_cycle })
    }

    # Here we are simply joining the line item to its variant
    # We dont use joins here to avoid the default scopes, and with that, include deleted variants
    scope :supplied_by_any, lambda { |enterprises|
      variant_ids = Spree::Variant.unscoped.where(supplier: enterprises).select(:id)
      where(variant_id: variant_ids)
    }

    scope :with_tax, -> {
      joins(:adjustments).
        where(spree_adjustments: { originator_type: 'Spree::TaxRate' }).
        select('DISTINCT spree_line_items.*')
    }

    # Line items without a Spree::TaxRate-originated adjustment
    scope :without_tax, -> {
      joins("
        LEFT OUTER JOIN spree_adjustments
          ON (spree_adjustments.adjustable_id=spree_line_items.id
            AND spree_adjustments.adjustable_type = 'Spree::LineItem'
            AND spree_adjustments.originator_type='Spree::TaxRate')").
        where(spree_adjustments: { id: nil })
    }

    scope :editable_by_producers, ->(enterprises_ids) {
      joins(variant: :supplier, order: :distributor).where(
        distributor: { enable_producers_to_edit_orders: true },
        spree_variants: { supplier_id: enterprises_ids }
      )
    }

    def copy_price
      return unless variant

      self.price = variant.price if price.nil?
      self.currency = variant.currency if currency.nil?
    end

    def copy_tax_category
      return unless variant

      self.tax_category = variant.tax_category
    end

    def copy_dimensions
      return unless variant

      self.weight ||= computed_weight_from_variant
      self.height ||= variant.height
      self.width ||= variant.width
      self.depth ||= variant.depth
    end

    def amount
      price * quantity
    end
    alias total amount

    def single_money
      Spree::Money.new(price, currency:)
    end
    alias single_display_amount single_money

    def money
      Spree::Money.new(amount, currency:)
    end
    alias display_total money
    alias display_amount money

    def adjust_quantity
      self.quantity = 0 if quantity.nil? || quantity < 0
    end

    # Here we skip stock check if skip_stock_check flag is active,
    #   we skip stock check if requested quantity is zero or negative,
    #   and we scope variants to hub and thus acivate variant overrides.
    def sufficient_stock?
      return true if skip_stock_check
      return true if quantity <= 0

      scoper.scope(variant)
      variant.can_supply?(quantity)
    end

    def insufficient_stock?
      !sufficient_stock?
    end

    def assign_stock_changes_to=(shipment)
      @preferred_shipment = shipment
    end

    def cap_quantity_at_stock!
      scoper.scope(variant)
      return if variant.on_demand

      update!(quantity: variant.on_hand) if quantity > variant.on_hand
    end

    def has_tax?
      adjustments.tax.any?
    end

    def included_tax
      adjustments.tax.inclusive.sum(:amount)
    end

    def added_tax
      adjustments.tax.additional.sum(:amount)
    end

    # Some of the tax rates may not be applicable depending to the order's tax zone
    def tax_rates
      variant&.tax_category&.tax_rates || []
    end

    def price_with_adjustments
      # EnterpriseFee#create_adjustment applies adjustments on line items to their parent order,
      # so line_item.adjustments returns an empty array
      return 0 if quantity.zero?

      fees = enterprise_fee_adjustments.sum(:amount)

      (price + (fees / quantity)).round(2)
    end

    def single_display_amount_with_adjustments
      Spree::Money.new(price_with_adjustments, currency:)
    end

    def amount_with_adjustments
      # We calculate from price_with_adjustments here rather than building our own value because
      # rounding errors can produce discrepencies of $0.01.
      price_with_adjustments * quantity
    end

    def display_amount_with_adjustments
      Spree::Money.new(amount_with_adjustments, currency:)
    end

    def display_included_tax
      Spree::Money.new(included_tax, currency:)
    end

    def unit_value
      return variant.unit_value if quantity == 0 || !final_weight_volume

      final_weight_volume / quantity
    end

    def unit_price
      unit_price = UnitPrice.new(variant)
      {
        amount: price_with_adjustments / unit_price.denominator,
        unit: unit_price.unit,
      }
    end

    def scoper
      @scoper ||= OpenFoodNetwork::ScopeVariantToHub.new(order.distributor)
    end

    def enterprise_fee_adjustments
      adjustments.enterprise_fee
    end

    private

    def computed_weight_from_variant
      if variant.product.variant_unit == "weight"
        variant.unit_value / variant.product.variant_unit_scale
      else
        variant.weight
      end
    end

    def update_inventory
      return unless changed?

      scoper.scope(variant)
      Spree::OrderInventory.new(order).verify(self, target_shipment)
    end

    def update_order
      return unless saved_changes.present? || destroyed?

      # update the order totals, etc.
      order.create_tax_charge!
    end

    def update_inventory_before_destroy
      # This is necessary before destroying the line item
      #   so that update_inventory will restore stock to the variant
      self.quantity = 0

      update_inventory

      # This is necessary after updating inventory
      #   because update_inventory may delete the last shipment in the order
      #   and that makes update_order fail if we don't reload the shipments
      order.shipments.reload
    end

    def calculate_final_weight_volume
      if final_weight_volume.present? && quantity_was > 0
        self.final_weight_volume = final_weight_volume * quantity / quantity_was
      elsif variant&.unit_value.present?
        self.final_weight_volume = variant&.unit_value&.* quantity
      end
    end
  end
end
