require_dependency 'email_token'
require_dependency 'trust_level'
require_dependency 'sql_builder'

class User < ActiveRecord::Base

  attr_accessible :name, :username, :password, :email, :bio_raw, :website

  has_many :posts
  has_many :notifications
  has_many :topic_users
  has_many :topics
  has_many :user_open_ids
  has_many :user_actions
  has_many :post_actions
  has_many :email_logs
  has_many :post_timings
  has_many :topic_allowed_users
  has_many :topics_allowed, through: :topic_allowed_users, source: :topic
  has_many :email_tokens
  has_many :views
  has_many :user_visits
  has_many :invites
  has_one :twitter_user_info
  belongs_to :approved_by, class_name: 'User'

  validates_presence_of :username
  validates_presence_of :email
  validates_uniqueness_of :email
  validate :username_validator
  validate :password_validator

  before_save :cook
  before_save :update_username_lower
  before_save :ensure_password_is_hashed
  after_initialize :add_trust_level

  after_save :update_tracked_topics

  after_create :create_email_token

  # Whether we need to be sending a system message after creation
  attr_accessor :send_welcome_message

  # This is just used to pass some information into the serializer
  attr_accessor :notification_channel_position

  def self.username_length
    3..15
  end

  def self.suggest_username(name)
    # If it's an email
    if name =~ /([^@]+)@([^\.]+)/
      name = Regexp.last_match[1]

      # Special case, if it's me @ something, take the something.
      name = Regexp.last_match[2] if name == 'me'
    end

    name.gsub!(/^[^A-Za-z0-9]+/, "")
    name.gsub!(/[^A-Za-z0-9_]+$/, "")
    name.gsub!(/[^A-Za-z0-9_]+/, "_")

    # Pad the length with 1s
    missing_chars = User.username_length.begin - name.length
    name << ('1' * missing_chars) if missing_chars > 0

    # Trim extra length
    name = name[0..User.username_length.end-1]

    i = 1
    attempt = name
    while !username_available?(attempt)
      suffix = i.to_s
      max_length = User.username_length.end - 1 - suffix.length
      attempt = "#{name[0..max_length]}#{suffix}"
      i+=1
    end
    attempt
  end

  def self.create_for_email(email, opts={})
    username = suggest_username(email)

    if SiteSetting.call_mothership?
      begin
        match, available, suggestion = Mothership.nickname_match?( username, email )
        username = suggestion unless match or available
      rescue => e
        Rails.logger.error e.message + "\n" + e.backtrace.join("\n")
      end
    end

    user = User.new(email: email, username: username, name: username)
    user.trust_level = opts[:trust_level] if opts[:trust_level].present?
    user.save!

    if SiteSetting.call_mothership?
      begin
        Mothership.register_nickname( username, email )
      rescue => e
        Rails.logger.error e.message + "\n" + e.backtrace.join("\n")
      end
    end

    user
  end

  def self.username_available?(username)
    lower = username.downcase
    !User.where(username_lower: lower).exists?
  end

  def self.username_valid?(username)
    u = User.new(username: username)
    u.username_format_validator
    !u.errors[:username].present?
  end

  def enqueue_welcome_message(message_type)
    return unless SiteSetting.send_welcome_message?
    Jobs.enqueue(:send_system_message, user_id: self.id, message_type: message_type)
  end

  def self.suggest_name(email)
    return "" unless email
    name = email.split(/[@\+]/)[0]
    name = name.sub(".", "  ")
    name.split(" ").collect{|word| word[0] = word[0].upcase; word}.join(" ")
  end

  def change_username(new_username)
    self.username = new_username

    if SiteSetting.call_mothership? and self.valid?
      begin
        Mothership.register_nickname( self.username, self.email )
      rescue Mothership::NicknameUnavailable
        return false
      rescue => e
        Rails.logger.error e.message + "\n" + e.backtrace.join("\n")
      end
    end

    self.save
  end

  # Use a temporary key to find this user, store it in redis with an expiry
  def temporary_key
    key = SecureRandom.hex(32)
    $redis.setex "temporary_key:#{key}", 1.week, id.to_s
    key
  end

  # Find a user by temporary key, nil if not found or key is invalid
  def self.find_by_temporary_key(key)
    user_id = $redis.get("temporary_key:#{key}")
    if user_id.present?
      User.where(id: user_id.to_i).first
    end
  end

  # tricky, we need our bus to be subscribed from the right spot
  def sync_notification_channel_position
    @unread_notifications_by_type = nil
    self.notification_channel_position = MessageBus.last_id('/notification')
  end

  def invited_by
    used_invite = invites.where("redeemed_at is not null").includes(:invited_by).first
    return nil unless used_invite.present?
    used_invite.invited_by
  end

  # Approve this user
  def approve(approved_by)
    self.approved = true
    self.approved_by = approved_by
    self.approved_at = Time.now
    enqueue_welcome_message('welcome_approved') if save
  end

  def self.email_hash(email)
    Digest::MD5.hexdigest(email.strip.downcase)
  end

  def email_hash
    User.email_hash(self.email)
  end

  def unread_notifications_by_type
    @unread_notifications_by_type ||= notifications.where("id > ? and read = false", seen_notification_id).group(:notification_type).count
  end

  def reload
    @unread_notifications_by_type = nil
    super
  end


  def unread_private_messages
    return 0 if unread_notifications_by_type.blank?
    return unread_notifications_by_type[Notification.Types[:private_message]] || 0
  end

  def unread_notifications
    result = 0
    unread_notifications_by_type.each do |k,v|
      result += v unless k == Notification.Types[:private_message]
    end
    result
  end

  def saw_notification_id(notification_id)
    User.update_all ["seen_notification_id = ?", notification_id], ["seen_notification_id < ?", notification_id]
  end

  def publish_notifications_state
    MessageBus.publish("/notification",
        {unread_notifications: self.unread_notifications,
         unread_private_messages: self.unread_private_messages},
        user_ids: [self.id] # only publish the notification to this user
      )
  end

  # A selection of people to autocomplete on @mention
  def self.mentionable_usernames
    User.select(:username).order('last_posted_at desc').limit(20)
  end

  def regular?
    (not admin?) and (not has_trust_level?(:moderator))
  end

  def password=(password)
    # special case for passwordless accounts
    unless password.blank?
      @raw_password = password
    end
  end

  def confirm_password?(password)
    return false unless self.password_hash && self.salt
    self.password_hash == hash_password(password,self.salt)
  end

  def update_last_seen!
    now = DateTime.now
    now_date = now.to_date

    # Only update last seen once every minute
    redis_key = "user:#{self.id}:#{now_date.to_s}"
    if $redis.setnx(redis_key, "1")
      $redis.expire(redis_key, SiteSetting.active_user_rate_limit_secs)

      if self.last_seen_at.nil? || self.last_seen_at.to_date < now_date
        # count it
        row_count = User.exec_sql('insert into user_visits(user_id,visited_at) select  :user_id, :visited_at
                      where not exists(select 1 from user_visits where user_id = :user_id and visited_at = :visited_at)', user_id: self.id, visited_at: now.to_date)
        if row_count.cmd_tuples == 1
          User.update_all "days_visited = days_visited + 1", ["id = ? and days_visited = ?", self.id, self.days_visited]
        end
      end

      # using a builder to avoid the AR transaction
      sql = SqlBuilder.new "update users /*set*/ where id = :id"
      # Keep track of our last visit
      if self.last_seen_at.present? and (self.last_seen_at < (now - SiteSetting.previous_visit_timeout_hours.hours))
        self.previous_visit_at = self.last_seen_at
        sql.set('previous_visit_at = :prev', prev: self.previous_visit_at)
      end
      self.last_seen_at = now
      sql.set('last_seen_at = :last', last: self.last_seen_at)
      sql.exec(id: self.id)
    end

  end

  def self.avatar_template(email)
    email_hash = self.email_hash(email)
    # robohash was possibly causing caching issues
    # robohash = CGI.escape("http://robohash.org/size_") << "{size}x{size}" << CGI.escape("/#{email_hash}.png")
    "http://www.gravatar.com/avatar/#{email_hash}.png?s={size}&r=pg&d=identicon"
  end

  # return null for local avatars, a template for gravatar
  def avatar_template
    # robohash = CGI.escape("http://robohash.org/size_") << "{size}x{size}" << CGI.escape("/#{email_hash}.png")
    "http://www.gravatar.com/avatar/#{email_hash}.png?s={size}&r=pg&d=identicon"
  end


  # Updates the denormalized view counts for all users
  def self.update_view_counts

    # Update denormalized topics_entered
    exec_sql "UPDATE users SET topics_entered = x.c
             FROM
            (SELECT v.user_id,
                    COUNT(DISTINCT parent_id) AS c
             FROM views AS v
             WHERE parent_type = 'Topic'
             GROUP BY v.user_id) AS X
            WHERE x.user_id = users.id"

    # Update denormalzied posts_read_count
    exec_sql "UPDATE users SET posts_read_count = x.c
              FROM
              (SELECT pt.user_id,
                      COUNT(*) AS c
               FROM post_timings AS pt
               GROUP BY pt.user_id) AS X
               WHERE x.user_id = users.id"

  end

  # The following count methods are somewhat slow - definitely don't use them in a loop.
  # They might need to be denormialzied
  def like_count
    UserAction.where(user_id: self.id, action_type: UserAction::WAS_LIKED).count
  end

  def post_count
    posts.count
  end

  def flags_given_count
    PostAction.where(user_id: self.id, post_action_type_id: PostActionType.FlagTypes).count
  end

  def flags_received_count
    posts.includes(:post_actions).where('post_actions.post_action_type_id in (?)', PostActionType.FlagTypes).count
  end

  def private_topics_count
    topics_allowed.where(archetype: Archetype.private_message).count
  end

  def bio_excerpt
    PrettyText.excerpt(bio_cooked, 350)
  end

  def delete_all_posts!(guardian)
    raise Discourse::InvalidAccess unless guardian.can_delete_all_posts? self

    posts.order("post_number desc").each do |p|
      if p.post_number == 1
        p.topic.destroy
      else
        p.destroy
      end
    end
  end

  def is_banned?
    !banned_till.nil? && banned_till > DateTime.now
  end

  # Use this helper to determine if the user has a particular trust level.
  # Takes into account admin, etc.
  def has_trust_level?(level)
    raise "Invalid trust level #{level}" unless TrustLevel.Levels.has_key?(level)

    # Admins can do everything
    return true if admin?

    # Otherwise compare levels
    (self.trust_level || TrustLevel.Levels[:new]) >= TrustLevel.Levels[level]
  end

  def guardian
    Guardian.new(self)
  end

  def username_format_validator
    validator = UsernameValidator.new(username)
    unless validator.valid_format?
      validator.errors.each { |e| errors.add(:username, e) }
    end
  end


  protected

    def cook
      if self.bio_raw.present?
        self.bio_cooked = PrettyText.cook(bio_raw) if bio_raw_changed?
      else
        self.bio_cooked = nil
      end
    end

    def update_tracked_topics
      if self.auto_track_topics_after_msecs_changed?

        if auto_track_topics_after_msecs < 0

          User.exec_sql('update topic_users set notification_level = ?
                         where notifications_reason_id is null and
                           user_id = ?' , TopicUser::NotificationLevel::REGULAR , self.id)
        else

          User.exec_sql('update topic_users set notification_level = ?
                         where notifications_reason_id is null and
                           user_id = ? and
                           total_msecs_viewed < ?' , TopicUser::NotificationLevel::REGULAR , self.id, auto_track_topics_after_msecs)

          User.exec_sql('update topic_users set notification_level = ?
                         where notifications_reason_id is null and
                           user_id = ? and
                           total_msecs_viewed >= ?' , TopicUser::NotificationLevel::TRACKING , self.id, auto_track_topics_after_msecs)
        end
      end
    end


    def create_email_token
      email_tokens.create(email: self.email)
    end

    def ensure_password_is_hashed
      if @raw_password
        self.salt = SecureRandom.hex(16)
        self.password_hash = hash_password(@raw_password, salt)
      end
    end

    def hash_password(password, salt)
      PBKDF2.new(:password => password, :salt => salt, :iterations => Rails.configuration.pbkdf2_iterations).hex_string
    end

    def add_trust_level
      self.trust_level ||= SiteSetting.default_trust_level
    rescue ActiveModel::MissingAttributeError
      # Ignore it, safely - see http://www.tatvartha.com/2011/03/activerecordmissingattributeerror-missing-attribute-a-bug-or-a-features/
    end

    def update_username_lower
      self.username_lower = username.downcase
    end

    def username_validator
      username_format_validator || begin
        lower = username.downcase
        if username_changed? && User.where(username_lower: lower).exists?
          return errors.add(:username, I18n.t(:'user.username.unique'))
        end
      end
    end

    def password_validator
      if @raw_password
        return errors.add(:password, "must be 6 letters or longer") if @raw_password.length < 6
      end
    end

end