class ClosetHanger < ApplicationRecord
  belongs_to :item
  belongs_to :list, class_name: 'ClosetList', optional: true, touch: true
  belongs_to :user

  delegate :name, to: :item, prefix: true
  delegate :log_trade_activity, to: :user

  validates :item_id, :uniqueness => {:scope => [:user_id, :owned, :list_id]}
  validates :quantity, :numericality => {:greater_than => 0}
  validates_presence_of :item, :user

  validate :list_belongs_to_user

  scope :alphabetical_by_item_name, -> {
    i = Item.arel_table
    joins(:item).order(i[:name].asc)
  }
  scope :trading, -> {
    ch = arel_table
    # sigh… our default-lists continue to be a pain
    cl = ClosetList.arel_table
    u = User.arel_table
    joins(:user).left_outer_joins(:list).where(
      ch[:list_id].not_eq(nil).and(cl[:visibility].gteq(
          ClosetVisibility[:trading].id))
    ).or(where(
      (
        ch[:list_id].eq(nil).and(ch[:owned].eq(true))
      ).and(
        u[:owned_closet_hangers_visibility].gteq(
          ClosetVisibility[:trading].id)
      )
    )).or(where(
      (
        ch[:list_id].eq(nil).and(ch[:owned].eq(false))
      ).and(
        u[:wanted_closet_hangers_visibility].gteq(
          ClosetVisibility[:trading].id)
      )
    ))
  }
  scope :newest, -> { order(arel_table[:created_at].desc) }
  scope :owned_before_wanted, -> { order(arel_table[:owned].desc) }
  scope :unlisted, -> { where(:list_id => nil) }
  scope :user_is_active, -> {
    u = User.arel_table
    joins(:user).where(u[:last_trade_activity_at].gteq(6.months.ago))
  }

  before_validation :merge_quantities, :set_owned_by_list

  after_save :log_trade_activity, if: :trading?
  after_destroy :log_trade_activity, if: :trading?

  def possibly_null_closet_list
    list || user.null_closet_list(owned)
  end

  def trading?
    possibly_null_closet_list.trading?
  end

  def wanted?
    !owned?
  end

  def possibly_null_list_id=(list_id_or_owned)
    if list_id_or_owned.to_s == 'true' || list_id_or_owned.to_s == 'false'
      self.list_id = nil
      self.owned = list_id_or_owned
    else
      self.list_id = list_id_or_owned
      # owned is set in the set_owned_by_list hook
    end
  end

  def verb(subject=:someone)
    self.class.verb(subject, owned?)
  end

  def self.verb(subject, owned, positive=true)
    base = (owned) ? 'own' : 'want'
    base << 's' if positive && subject != :you && subject != :i
    base
  end

  # TODO: Is the performance improvement on this actually much better than just
  # `includes`, now that `Item::Translation` records aren't part of it anymore?
  def self.preload_items(
    hangers,
    items_scope: Item.all
  )
    # Preload the records we need. (This is like `includes`, but `includes`
    # always selects all fields for all records, and we give the caller the
    # opportunity to specify which fields it actually wants via scope!)
    items = items_scope.where(id: hangers.map(&:item_id))

    # Group the records by relevant IDs.
    items_by_id = items.to_h { |i| [i.id, i] }

    # Assign the preloaded records to the records they belong to. (This is like
    # doing e.g. h.item = ..., but that's a database write - we actually just
    # want to set the `item` field itself directly! Hacky, ripped from how
    # `ActiveRecord::Associations::Preloader` does it!)
    hangers.each do |hanger|
      hanger.association(:item).target = items_by_id[hanger.item_id]
    end
  end
  
  def self.set_quantity!(quantity, options)
    quantity = quantity.to_i
    conditions = {:user_id => options[:user_id].to_i,
      :item_id => options[:item_id].to_i}
    
    if options[:key] == "true"
      conditions[:owned] = true
      conditions[:list_id] = nil
    elsif options[:key] == "false"
      conditions[:owned] = false
      conditions[:list_id] = nil
    else
      conditions[:list_id] = options[:key].to_i
    end
    
    hanger = self.where(conditions).first
    
    if quantity > 0
      # If quantity is non-zero, create/update the corresponding hanger.
      
      unless hanger
        hanger = self.new
        hanger.user_id = conditions[:user_id]
        hanger.item_id = conditions[:item_id]
        # One of the following will be nil, and that's okay. If owned is nil,
        # we'll cover for it before validation, as always.
        hanger.owned   = conditions[:owned]
        hanger.list_id = conditions[:list_id]
      end
      
      hanger.quantity = quantity
      hanger.save!
    elsif hanger
      # If quantity is zero and there's a hanger, destroy it.
      hanger.destroy
    end
    
    # If quantity is zero and there's no hanger, good. Do nothing.
  end

  # Use this with a scoped relation to convert it into a list of trades, e.g.
  # `item.hangers.trading.to_trades`.
  #
  # A trade includes the user who's trading, and the available closet hangers
  # (which you can use to get e.g. the list name).
  #
  # We don't preload anything here - if you want user names or list names, you
  # should `includes` them in the hanger scope first, to avoid extra queries!
  def self.to_trades
    # Let's ensure that the `trading` filter is applied, to avoid data leaks.
    # (I still recommend doing it at the call site too for clarity, though!)
    all_trading_hangers = trading.to_a

    owned_hangers = all_trading_hangers.filter(&:owned?)
    wanted_hangers = all_trading_hangers.filter(&:wanted?)

    # Group first into offering vs seeking, then by user.
    offering, seeking = [owned_hangers, wanted_hangers].map do |hangers|
      hangers.group_by(&:user_id).map do |user_id, user_hangers|
        Trade.new(user_id, user_hangers)
      end
    end

    {offering: offering, seeking: seeking}
  end

  Trade = Struct.new('Trade', :user_id, :hangers) do
    def user
      # Take advantage of `includes(:user)` on the hangers, if applied.
      hangers.first.user
    end

    def lists
      hangers.map(&:list).filter(&:present?)
    end
  end

  protected

  def list_belongs_to_user
    if list_id?
      if list
        errors.add(:list_id, "must belong to you") unless list.user_id == user_id
      else
        errors.add(:list, "must exist")
      end
    end
  end
  
  def merge_quantities
    # Find a hanger that conflicts: for the same item, in the same user's
    # closet, same owned status, same list. It also must not be the current
    # hanger. Select enough for our logic and to update flex_source.
    # TODO: We deleted flex, does this reduce what data we need here?
    conflicting_hanger = self.class.select([:id, :quantity, :user_id, :item_id,
                                            :owned, :list_id]).
      where(:user_id => user_id, :item_id => item_id, :owned => owned,
        :list_id => list_id).where(['id != ?', self.id]).first
    
    # If there is such a hanger, remove it and merge its quantity into this one.
    if conflicting_hanger
      self.quantity += conflicting_hanger.quantity
      conflicting_hanger.destroy
    end
    
    true
  end

  def set_owned_by_list
    self.owned = list.hangers_owned if list
    true
  end
end