class User < ApplicationRecord
  include PrettyParam

  PreviewTopContributorsCount = 3

  belongs_to :auth_user, foreign_key: :remote_id, inverse_of: :user
  delegate :disconnect_neopass, :uses_neopass?, to: :auth_user

  has_many :closet_hangers
  has_many :closet_lists
  has_many :closeted_items, through: :closet_hangers, source: :item
  has_many :contributions
  has_many :neopets_connections
  has_many :outfits

  # TODO: When `owned_items` and `wanted_items` are merged, they override one
  # another instead of correctly returning an empty set. Is this a Rails bug
  # that gets fixed down the line once we finish upgrading, or...?
  has_many :owned_items, -> { where(ClosetHanger.arel_table[:owned].eq(true)) },
    through: :closet_hangers, source: :item
  has_many :wanted_items, -> { where(ClosetHanger.arel_table[:owned].eq(false)) },
    through: :closet_hangers, source: :item

  belongs_to :contact_neopets_connection, class_name: 'NeopetsConnection', optional: true

  scope :top_contributors, -> { order('points DESC').where('points > 0') }

  after_update :sync_name_with_auth_user!, if: :saved_change_to_name?
  after_update :log_trade_activity, if: -> user {
    (user.saved_change_to_owned_closet_hangers_visibility? &&
      user.owned_closet_hangers_visibility >= ClosetVisibility[:trading].id) ||
    (user.saved_change_to_wanted_closet_hangers_visibility? &&
      user.wanted_closet_hangers_visibility >= ClosetVisibility[:trading].id)
  }

  def sync_name_with_auth_user!
    auth_user.name = name
    auth_user.save!
  end

  def admin?
    name == 'matchu' # you know that's right.
  end

  def as_json
    serializable_hash only: [:id, :name]
  end

  # Given info about a request, return whether that request is likely to be
  # coming from the same person who owns this account.
  def likely_is?(current_user, remote_ip)
    current_user == self || auth_user.current_sign_in_ip == remote_ip
  end

  def unowned_items
    # Join all items against our owned closet hangers, group by item ID, then
    # only return those with zero matching hangers.
    #
    # TODO: It'd be nice to replace this with a `left_outer_joins` call in
    # Rails 5+, but these conditions really do need to be part of the join:
    # if we do them as a `where`, they prevent unmatching items from being
    # returned in the first place.
    #
    # TODO: This crashes the query when combined with `unwanted_items`.
    ch = ClosetHanger.arel_table.alias("owned_hangers")
    Item.
      joins(
        "LEFT JOIN closet_hangers owned_hangers ON owned_hangers.item_id = items.id " + 
        "AND #{ch[:user_id].eq(self.id).to_sql} AND owned_hangers.owned = true"
      ).
      group("items.id").having("COUNT(owned_hangers.id) = 0")
  end

  def unwanted_items
    # See `unowned_items` above! We just change the `true` to `false`.
    # TODO: This crashes the query when combined with `unowned_items`.
    ch = ClosetHanger.arel_table.alias("wanted_hangers")
    Item.
      joins(
        "LEFT JOIN closet_hangers wanted_hangers ON wanted_hangers.item_id = items.id " + 
        "AND #{ch[:user_id].eq(self.id).to_sql} AND wanted_hangers.owned = false"
      ).
      group("items.id").having("COUNT(wanted_hangers.id) = 0")
  end

  def contribute!(pet)
    new_contributions = []
    pet.contributables.each do |contributable|
      if contributable.new_record?
        contribution = Contribution.new
        contribution.contributed = contributable
        contribution.user = self
        new_contributions << contribution
      end
    end
    new_points = 0 # temp assignment for scoping
    Pet.transaction do
      pet.save!
      new_contributions.each do |contribution|
        Rails.logger.debug("Saving contribution of #{contribution.contributed.inspect}: #{contribution.contributed_type.inspect}, #{contribution.contributed_id.inspect}")
        begin
          contribution.save!
        rescue ActiveRecord::RecordNotSaved => e
          raise ActiveRecord::RecordNotSaved, "#{e.message}, #{contribution.inspect}, #{contribution.valid?.inspect}, #{contribution.errors.inspect}"
        end
      end
      new_points = new_contributions.map(&:point_value).inject(0, &:+)
      self.points += new_points
      begin
        save!
      rescue ActiveRecord::RecordNotSaved => e
        raise ActiveRecord::RecordNotSaved, "#{e.message}, #{self.inspect}, #{self.valid?.inspect}, #{self.errors.inspect}"
      end
    end
    new_points
  end

  def assign_closeted_to_items!(items)
    # Assigning these items to a hash by ID means that we don't have to go
    # N^2 searching the items list for items that match the given IDs or vice
    # versa, and everything stays a lovely O(n)
    items_by_id = items.group_by(&:id)
    closet_hangers.where(:item_id => items_by_id.keys).each do |hanger|
      items = items_by_id[hanger.item_id]
      items.each do |item|
        if hanger.owned?
          item.owned = true
        else
          item.wanted = true
        end
      end
    end
  end

  def closet_hangers_groups_visible_to(user)
    if user == self
      [true, false]
    else
      public_closet_hangers_groups
    end
  end
  
  def public_closet_hangers_groups
    [].tap do |groups|
      groups << true if owned_closet_hangers_visibility >= ClosetVisibility[:public].id
      groups << false if wanted_closet_hangers_visibility >= ClosetVisibility[:public].id
    end
  end

  def null_closet_list(owned)
    owned ? null_owned_list : null_wanted_list
  end

  def null_owned_list
    ClosetList::NullOwned.new(self)
  end

  def null_wanted_list
    ClosetList::NullWanted.new(self)
  end

  def find_closet_list_by_id_or_null_owned(id_or_owned)
    id_or_owned_str = id_or_owned.to_s
    if id_or_owned_str == 'true'
      null_owned_list
    elsif id_or_owned_str == 'false'
      null_wanted_list
    else
      self.closet_lists.find id_or_owned
    end
  end

  def neopets_usernames
    neopets_connections.map(&:neopets_username)
  end

  def contact_neopets_username?
    contact_neopets_connection.present?
  end

  def contact_neopets_username
    contact_neopets_connection.try(:neopets_username)
  end

  def item_quantities_for(item_id)
    quantities = Hash.new(0)

    hangers = closet_hangers.where(item_id: item_id).
      select([:owned, :list_id, :quantity])
    hangers.each do |hanger|
      quantities[hanger.list_id || hanger.owned?] = hanger.quantity
    end

    quantities
  end

  def log_trade_activity
    touch(:last_trade_activity_at)
  end

  def self.points_required_to_pass_top_contributor(offset)
    user = User.top_contributors.select(:points).limit(1).offset(offset).first
    user ? user.points : 0
  end
end