class Message(db.Model, Base):
    __tablename__ = 'message'

    id = db.Column(db.Integer, primary_key=True)
    from_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    to_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    to_player_id = db.Column(db.Integer, db.ForeignKey('player.id'))
    sent_at = db.Column(db.DateTime, default=datetime.utcnow)
    seen_at = db.Column(db.DateTime)
    notified_at = db.Column(db.DateTime)
    body = db.Column(db.Text())
    body_html = db.Column(db.Text())
    user_ip = db.Column(db.String(15))
    deleted = db.Column(db.Boolean, default=False)

    from_user = db.relationship('User',
                                foreign_keys='Message.from_user_id',
                                backref=db.backref('messages_sent'))
    to_user = db.relationship('User',
                              foreign_keys='Message.to_user_id',
                              backref=db.backref('messages_received'))
    to_player = db.relationship('Player',
                                foreign_keys='Message.to_player_id',
                                backref=db.backref('messages_received'))

    def save(self, commit=True):
        from standardweb.lib import forums
        self.body_html = forums.convert_bbcode(self.body)

        for pat, path in forums.emoticon_map:
            self.body_html = pat.sub(path, self.body_html)

        return super(Message, self).save(commit)

    def to_dict(self):
        result = {
            'id':
            self.id,
            'sent_at':
            self.sent_at.replace(tzinfo=pytz.UTC).isoformat(),
            'seen_at':
            self.seen_at.replace(
                tzinfo=pytz.UTC).isoformat() if self.seen_at else None,
            'from_user':
            self.from_user.to_dict(),
            'body_html':
            self.body_html
        }

        if self.to_user:
            result['to_user'] = self.to_user.to_dict()

        return result
class ForumPostVote(db.Model, Base):
    __tablename__ = 'forum_post_vote'

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    post_id = db.Column(db.Integer, db.ForeignKey('forum_post.id'))
    vote = db.Column(db.Integer(), default=0)
    user_ip = db.Column(db.String(15))
    computed_weight = db.Column(db.Numeric())
    created = db.Column(db.DateTime, default=datetime.utcnow)
    updated = db.Column(db.DateTime, default=None)

    post = db.relationship('ForumPost', backref=db.backref('votes'))
    user = db.relationship('User', backref=db.backref('votes'))
class GroupInvite(db.Model, Base):
    __tablename__ = 'group_invite'

    group_id = db.Column(db.Integer,
                         db.ForeignKey('group.id'),
                         primary_key=True)
    invite = db.Column(db.String(30), primary_key=True)

    group = db.relationship('Group', backref=db.backref('invites'))
class Title(db.Model, Base):
    __tablename__ = 'title'

    id = db.Column(db.Integer, primary_key=True)
    created = db.Column(db.DateTime, default=datetime.utcnow)
    name = db.Column(db.String(20))
    displayname = db.Column(db.String(40))
    broadcast = db.Column(db.Boolean, default=False)

    players = db.relationship('Player',
                              secondary=player_title,
                              backref=db.backref('titles'))
class ForumBan(db.Model, Base):
    __tablename__ = 'forum_ban'

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    ban_start = db.Column(db.DateTime, default=datetime.utcnow)
    reason = db.Column(db.Text())
    by_user_id = db.Column(db.Integer, db.ForeignKey('user.id'))

    user = db.relationship('User',
                           foreign_keys='ForumBan.user_id',
                           backref=db.backref('forum_ban', uselist=False))
    by_user = db.relationship('User', foreign_keys='ForumBan.by_user_id')
class ForumAttachment(db.Model, Base):
    __tablename__ = 'forum_attachment'

    id = db.Column(db.Integer, primary_key=True)
    post_id = db.Column(db.Integer, db.ForeignKey('forum_post.id'))
    size = db.Column(db.Integer())
    content_type = db.Column(db.String(255))
    path = db.Column(db.String(255))
    name = db.Column(db.Text())
    hash = db.Column(db.String(40))

    post = db.relationship('ForumPost', backref=db.backref('attachments'))

    @classmethod
    def create_attachment(cls, post_id, image, commit=True):
        try:
            content_type = image.headers.get('Content-Type')
            image_content = image.content
            size = len(image_content)
            path = str(post_id)

            attachment = cls(post_id=post_id,
                             size=size,
                             content_type=content_type,
                             path=path,
                             name=image.filename)
            attachment.save(commit=commit)

            with open(attachment.file_path, 'w') as f:
                f.write(image_content)

            return attachment
        except:
            return None

    def save(self, commit=True):
        import hashlib

        self.hash = hashlib.sha1(self.path +
                                 app.config['SECRET_KEY']).hexdigest()

        return super(ForumAttachment, self).save(commit)

    @property
    def url(self):
        return url_for('forum_attachment', hash=self.hash)

    @property
    def file_path(self):
        return os.path.join(app.root_path, 'attachments', self.path)
class ForumPostTracking(db.Model, Base):
    __tablename__ = 'forum_posttracking'

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    topics = db.Column(db.Text(), default=None)
    last_read = db.Column(db.DateTime, default=None)

    user = db.relationship('User',
                           backref=db.backref('posttracking', uselist=False))

    def get_topics(self):
        try:
            return json.loads(self.topics) if self.topics else None
        except ValueError:
            return None

    def set_topics(self, topics):
        self.topics = json.dumps(topics)
class ForumProfile(db.Model, Base):
    __tablename__ = 'forum_profile'

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    post_count = db.Column(db.Integer, default=0)
    signature = db.Column(db.Text())
    signature_html = db.Column(db.Text())

    user = db.relationship('User',
                           backref=db.backref('forum_profile', uselist=False))

    @property
    def last_post(self):
        post = ForumPost.query.filter(
            ForumPost.deleted == False,
            ForumPost.user_id == self.user_id).order_by(
                ForumPost.created.desc()).limit(1).first()

        return post
class User(db.Model, Base):
    __tablename__ = 'user'

    id = db.Column(db.Integer, primary_key=True)
    username = db.Column(db.String(30))
    player_id = db.Column(db.Integer, db.ForeignKey('player.id'))
    uuid = db.Column(db.String(32))
    full_name = db.Column(db.String(50))
    email = db.Column(db.String(75))
    password = db.Column(db.String(128))
    admin = db.Column(db.Boolean, default=False)
    moderator = db.Column(db.Boolean, default=False)
    mfa_login = db.Column(db.Boolean, default=False)
    mfa_secret = db.Column(db.String(20))
    score = db.Column(db.Numeric(), default=0)
    last_login = db.Column(db.DateTime, default=None)
    date_joined = db.Column(db.DateTime, default=datetime.utcnow)

    player = db.relationship('Player',
                             backref=db.backref('user', uselist=False))

    @property
    def admin_or_moderator(self):
        return self.admin or self.moderator

    @classmethod
    def create(cls, player, plaintext_password, email):
        user = cls(player=player, uuid=player.uuid)
        user.set_password(plaintext_password)
        user.last_login = datetime.utcnow()
        user.email = email

        user.save(commit=False)

        forum_profile = ForumProfile(user=user)
        forum_profile.save(commit=True)

        # make sure messages/notifications received to this player are properly
        # associated with the newly created user
        Message.query.filter_by(to_player=player,
                                to_user=None).update({'to_user_id': user.id})

        Notification.query.filter_by(player=player,
                                     user=None).update({'user_id': user.id})

        db.session.commit()

        return user

    def to_dict(self):
        result = {'username': self.username}

        if self.player_id:
            result['player'] = self.player.to_dict()

        return result

    def check_password(self, plaintext_password):
        algorithm, iterations, salt, hash_val = self.password.split('$', 3)
        expected = User._make_password(plaintext_password,
                                       salt=salt,
                                       iterations=int(iterations))

        return h.safe_str_cmp(self.password, expected)

    def set_password(self, plaintext_password, commit=True):
        password = User._make_password(plaintext_password)
        self.password = password
        self.save(commit=commit)

    def get_username(self):
        if self.player_id:
            return self.player.username

        return self.username

    def get_unread_notification_count(self):
        return len(
            Notification.query.with_entities(Notification.id).filter_by(
                user=self, seen_at=None).all())

    def get_unread_message_count(self):
        return len(
            Message.query.with_entities(Message.id).filter_by(
                to_user=self, seen_at=None, deleted=False).all())

    def get_notification_preferences(self, create=True, can_commit=True):
        from standardweb.lib import notifications

        preferences = NotificationPreference.query.filter_by(user=self).all()

        if create:
            active_preference_names = set()
            for preference in preferences:
                active_preference_names.add(preference.name)

            missing_preference_names = notifications.NOTIFICATION_NAMES - active_preference_names
            for name in missing_preference_names:
                preference = NotificationPreference(user=self, name=name)

                preference.save(commit=False)

                preferences.append(preference)

            if can_commit:
                db.session.commit()

        return sorted(preferences, key=attrgetter('name'))

    def get_notification_preference(self, type, create=True, can_commit=True):
        preference = NotificationPreference.query.filter_by(user=self,
                                                            name=type).first()

        if not preference and create:
            preference = NotificationPreference(user=self, name=type)

            preference.save(commit=can_commit)

        return preference

    @property
    def has_excellent_score(self):
        return self.score > app.config['EXCELLENT_SCORE_THRESHOLD']

    @property
    def has_great_score(self):
        return self.score > app.config[
            'GREAT_SCORE_THRESHOLD'] and not self.has_excellent_score

    @property
    def has_good_score(self):
        return self.score > app.config[
            'GOOD_SCORE_THRESHOLD'] and not self.has_great_score

    @property
    def has_bad_score(self):
        return self.score < app.config[
            'BAD_SCORE_THRESHOLD'] and not self.has_terrible_score

    @property
    def has_terrible_score(self):
        return self.score < app.config[
            'TERRIBLE_SCORE_THRESHOLD'] and not self.has_abysmal_score

    @property
    def has_abysmal_score(self):
        return self.score < app.config['ABYSMAL_SCORE_THRESHOLD']

    @classmethod
    def _make_password(cls, password, salt=None, iterations=None):
        if not salt:
            salt = binascii.b2a_hex(os.urandom(15))

        if not iterations:
            iterations = 10000

        hash_val = pbkdf2_bin(password.encode('utf-8'),
                              salt,
                              iterations,
                              keylen=32,
                              hashfunc=hashlib.sha256)
        hash_val = hash_val.encode('base64').strip()
        return '%s$%s$%s$%s' % ('pbkdf2_sha256', iterations, salt, hash_val)
class Notification(db.Model, Base):
    __tablename__ = 'notification'

    id = db.Column(db.Integer, primary_key=True)
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
    player_id = db.Column(db.Integer, db.ForeignKey('player.id'))
    timestamp = db.Column(db.DateTime, default=datetime.utcnow)
    type = db.Column(db.String(30))
    seen_at = db.Column(db.DateTime)
    data = db.Column(JsonEncodedDict, default={})

    user = db.relationship('User',
                           foreign_keys='Notification.user_id',
                           backref=db.backref('notifications'))

    player = db.relationship('Player',
                             foreign_keys='Notification.player_id',
                             backref=db.backref('notifications'))

    _definition = None

    @classmethod
    def create(cls,
               type,
               data=None,
               user_id=None,
               player_id=None,
               send_email=True,
               **kw):
        from standardweb.lib import notifier

        if data is None:
            data = {}

        data.update(**kw)

        notification = cls(type=type,
                           user_id=user_id,
                           player_id=player_id,
                           data=data)

        notification.definition
        notification.save(commit=True)

        notifier.notification_notify(notification, send_email=send_email)

        return notification

    @property
    def definition(self):
        from standardweb.lib import notifications

        if not self._definition:
            self._definition = notifications.validate_notification(
                self.type, self.data)

        return self._definition

    @property
    def description(self):
        return self.definition.get_html_description(self.data)

    @property
    def can_notify_ingame(self):
        return self.definition.can_notify_ingame