# Schema as of Mon Nov 27 15:17:56 EST 2006 (schema version 35)
#
#  id                  :integer(11)   not null
#  name                :string(255)   
#  type                :string(255)   
#  email               :string(100)   
#  crypted_password    :string(40)    
#  salt                :string(40)    
#  created_at          :datetime      
#  bio                 :text          
#  picture             :string(255)   
#  user_number         :string(255)   
#  year_of_birth       :integer(11)   
#  gender              :integer(11)   default(0)
#  postal_code         :string(255)   
#  login_key           :string(255)   
#  verified            :boolean(1)    
#  login_key_expires_at:datetime      
#

require 'openssl'
require 'base64'
require 'digest/sha1'

class RegisteredUser < User
  
  has_many :comments, :foreign_key => 'created_by'
  has_many :rateds,   :through => :comments, :source => :ratings

  # Virtual attribute for the unencrypted password
  attr_accessor :password

  validates_presence_of     :password_confirmation,
                            :if => :password_required?
  validates_length_of       :password, :within => 5..40, :if => :password_required?
  validates_confirmation_of :password,                   :if => :password_required?
  validates_format_of       :email, :with => RFC822::EmailAddress
  
  validates_uniqueness_of   :email
  
  before_validation         :encrypt_password
    
  # Authenticates a user by their email and unencrypted password.  Returns the user or nil.
  # (from Beast) we allow false to be passed in so a failed login can check for an inactive
  # account to show a different error
  def self.authenticate(email, password, verified=true)
    # use this instead if you want user activation
    # u = find :first, :select => 'id, salt', :conditions => ['login = ? and activated_at IS NOT NULL', login]
    return nil if password.blank?
    #@@@ FIXME for some reason this class isn't seeing the classes that inherit from it, and is thus excluding them from the find SQL
    [Expert, Participant, Admin].each{|c| c.new}
    u = self.find_by_email_and_verified(email, verified) # need to get the salt
    u && u.authenticated?(password) && !u.suspended? ? u : nil
  end
  
  def self.suspended?(email)
    u = find_by_email(email)
    u && u.suspended?
  end
  
  # More extra credit for adding 2-way encryption.  Feel free to remove self.encrypt above if you use this
  #
  # Encrypts some data with the salt.
  def self.encrypt(password, salt)
    enc = OpenSSL::Cipher::Cipher.new('DES-EDE3-CBC')
    enc.encrypt(salt)
    data = enc.update(password)
    Base64.encode64(data << enc.final)
  end
  
  # Encrypts the password with the user salt
  def encrypt(password)
    self.class.encrypt(password, salt)
  end  

  def authenticated?(password)
    crypted_password == encrypt(password)
  end

  def login_key?
    login_key_expires_at && Time.now.utc < login_key_expires_at 
  end

  # These create and unset the fields required for remembering users between browser closes
  def remember_me
    self.login_key_expires_at = 2.weeks.from_now.utc
    self.login_key            = encrypt("#{email}--#{login_key_expires_at}")
    save(false)
  end

  def forget_me
    self.login_key_expires_at = nil
    self.login_key            = nil
    save(false)
  end
  
  def self.user_number_taken?(user_number)
    return self.find_by_user_number(user_number)
  end
  
  # getter method to decrypt password
  def password
    unless @password
      enc = OpenSSL::Cipher::Cipher.new('DES-EDE3-CBC')
      enc.decrypt(salt)
      text = enc.update(Base64.decode64(crypted_password))
      @password = (text << enc.final)
    end
    @password
  rescue
    nil
  end
  
  # lifted from Beast
  # http://svn.techno-weenie.net/projects/beast/trunk/app/models/user.rb
  def reset_login_key!
    # self.login_key = Digest::SHA1.hexdigest(Time.now.to_s + password_hash.to_s + rand(123456789).to_s).to_s
    self.login_key = Digest::SHA1.hexdigest(Time.now.to_s + crypted_password.to_s + rand(123456789).to_s).to_s
    # this is not currently honored
    # self.login_key_expires_at = Time.now.utc+1.year
    # save!
    save(false) # don't try to validate -- just save
    login_key
  end
      
  def email_address_with_name
    "#{name} <#{email}>"
  end
  
  # see Participant#take_activity_from_guest_account! for real implementation
  # this allows guests to "read", then login as admin or experts -- users who don't track reads
  def take_activity_from_guest_account!(previously_held_guest_account)
    previously_held_guest_account.destroy if previously_held_guest_account.is_a?(Guest)
  end

  def registered?
    true
  end
  
  # Returns the province if found, or nil if not
  def province
    Provinces::full_name(PostalCode::province(postal_code))
  end

  # Recalculate the average rating across all my comments and update the avg_rating field.
  #
  # Untill there are more than two ratings, the average stays at zero.
  def update_average_rating_cache
    update_attribute(:avg_rating, rateds.count > 2 ? average_rating_value_across_all_ratings : 0)
  end
  
  # Approach 1. find all the ratings and average them (all ratings count equal)
  # 
  # This overhead for this calculation could be reduced by keeping a running tally of 
  # the total rating score (ignoring any 77s) and a ratings count, then simply dividing 
  # when called for
  def average_rating_value_across_all_ratings
    rateds.average_value
  end
  
  # Approach 2. find all comments and average the comment average rating (all comments count equal)
  def average_comment_rating_value_across_all_comments
    comments.average(:avg_rating, :condition => 'avg_rating != 0')
  end
  
  class << self
    
    # Return the best 10 average rated users.
    #
    # "Best" in this case means the lowest non-zero score. This is because the best rating to get
    # is a 1, while the worst is a 4.
    def find_ten_best_avg_rating
      find(:all, :order => 'avg_rating ASC', :conditions => 'avg_rating > 0', :limit => 10)
    end
  end
  
 protected
 
  # before filter 
  def encrypt_password
    return if @password.blank?
    self.salt = Digest::SHA1.hexdigest("--#{Time.now.to_s}--#{email}--") if new_record? || self.salt.blank?
    self.crypted_password = encrypt(@password)
  end
  
  def password_required?
    crypted_password.blank? or not @password.blank?
  end
  
end
