from datetime import date, datetime
from app import app, db
from flask_login import UserMixin
from app.models.contents import Content
from app.models.privs import SpecialPrivilege, Group, GroupMember, \
    GroupPrivilege
from config import V5Config

import werkzeug.security
import app
import re

# User: Website user that performs actions on the content
class User(UserMixin, db.Model):
    __tablename__ = 'user'

    # User ID, should be used to refer to any user. Thea actual user can either
    # be a guest (with IP as key) or a member (with this ID as key).
    id = db.Column(db.Integer, primary_key=True)
    # User type (polymorphic discriminator)
    type = db.Column(db.String(30))

    # TODO: add good relation
    contents = db.relationship('Content', back_populates="author")

    __mapper_args__ = {
        'polymorphic_identity': __tablename__,
        'polymorphic_on': type
    }

    def __repr__(self):
        return f'<User #{self.id}>'

    @staticmethod
    def valid_name(name):
        """
        Checks whether a string is a valid user name. The criteria are:
        1. No whitespace-class character
        2. At least one letter
        3. At least 3 characters and no longer than 32 characters

        Possibily other intresting criteria:
        4. Unicode restriction
        """

        if type(name) != str or len(name) < 3 or len(name) > 32:
            return False
        if name in V5Config.FORBIDDEN_USERNAMES:
            return False
        # Reject all Unicode whitespaces. This is important to avoid the most
        # common Unicode tricks!
        if re.search(r'\s', name) is not None:
            return False
        # There must be at least one letter (avoid complete garbage)
        if re.search(r'\w', name) is None:
            return False

        return True

# Guest: Unregistered user with minimal privileges
class Guest(User, db.Model):
    __tablename__ = 'guest'
    __mapper_args__ = { 'polymorphic_identity': __tablename__ }

    # ID of the [User] entry
    id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)
    # Reusable username, can be the name of a member (will be distinguished at
    # rendering time)
    username = db.Column(db.Unicode(64), index=True)
    # IP address, 47 characters is the max for an IPv6 address
    ip = db.Column(db.String(47))

    def __repr__(self):
        return f'<Guest: {self.username} ({self.ip})>'

# Member: Registered user with full access to the website's services
class Member(User, db.Model):
    __tablename__ = 'member'
    __mapper_args__ = { 'polymorphic_identity': __tablename__ }

    # Id of the [User] entry
    id = db.Column(db.Integer, db.ForeignKey('user.id'), primary_key=True)

    # Primary attributes (needed for the system to work)
    name          = db.Column(db.Unicode(32), index=True, unique=True)
    email         = db.Column(db.Unicode(120), index=True, unique=True)
    password_hash = db.Column(db.String(255))
    xp            = db.Column(db.Integer)
    innovation    = db.Column(db.Integer)
    register_date = db.Column(db.Date, default=date.today)

    # Avatars # TODO: rendre ça un peu plus propre
    @property
    def avatar(self):
        return 'avatars/' + str(self.id) + '.png'

    # Groups and related privileges
    groups = db.relationship('Group', secondary=GroupMember,
        back_populates='members')

    # Personal information, all optional
    bio       = db.Column(db.UnicodeText)
    signature = db.Column(db.UnicodeText)
    birthday  = db.Column(db.Date)

    # Settings
    newsletter = db.Column(db.Boolean, default=False)

    # Relations
    # trophies = db.relationship('Trophy', back_populates='member')
    # tests    = db.relationship('Test', back_populates='author')

    def __init__(self, name, email, password):
        """Register a new user."""
        if not User.valid_name(name):
            raise Exception(f'{name} is not a valid user name')

        self.name = name
        self.email = email
        self.set_password(password)
        self.xp = 0
        self.innovation = 0

        self.bio = ""
        self.signature = ""
        self.birthday = None

    def priv(self, priv):
        """Check whether the member has the specified privilege."""
        if SpecialPrivilege.filter(uid=self.id, priv=priv):
            return True
        return False
#        return db.session.query(User, Group, GroupPrivilege).filter(
#            Group.id.in_(User.groups), GroupPrivilege.gid==Group.id,
#            GroupPrivilege.priv==priv).first() is not None

    def update(self, **data):
        """
        Update all or part of the user's metadata. The [data] dictionary
        accepts the following keys:
          "name"        str    User name
          "email"       str    User mail ddress
          "password"    str    Raw password
          "bio"         str    Biograpy
          "signature"   str    Post signature
          "birthday"    date   Birthday date
          "newsletter"  bool   Newsletter setting
        For future compatibility, other attributes are silently ignored. None
        values can be specified and are ignored.

        It is the caller's responsibility to check that the request sender has
        the right to change user names, password... otherwise this method will
        turn out dangerous!
        """

        data = { key: data[key] for key in data if data[key] is not None }

        if "name" in data:
            if not User.valid_name(data["name"]):
                raise Exception(f'{data["name"]} is not a valid user name')
            self.name = data["name"]

        # TODO: verify good type of those args, think about the password mgt
        if "email" in data:
            self.email = data["email"]
        if "password" in data:
            self.set_password(data["password"])
        if "bio" in data:
            self.bio = data["bio"]
        if "signature" in data:
            self.signature = data["signature"]
        if "birthday" in data:
            self.birthday = data["birthday"]
        if "newsletter" in data:
            self.newsletter = data["newsletter"]

    def get_public_data(self):
        """Returns the public information of the member."""
        return {
            "name":          self.name,
            "xp":            self.xp,
            "innovation":    self.innovation,
            "register_date": self.register_date,
            "bio":           self.bio,
            "signature":     self.signature,
            "birthday":      self.birthday,
        }

    def add_xp(self, amount):
        """
        Reward xp to a member. If [amount] is negative, the xp total of the
        member will decrease, down to zero.
        """
        self.xp_points = max(self.xp_points + amount, 0)

    def add_innovation(self, n):
        """
        Reward innovation points to a member. If [amount] is negative, the
        innovation points total will decrease, down to zero.
        """
        self.innovation = max(self.innovation + amount, 0)

    def set_password(self, password):
        """
        Set the user's password. Check whether the request sender has the right
        to do this!
        """
        self.password_hash = werkzeug.security.generate_password_hash(password,
            method='pbkdf2:sha512', salt_length=10)

    def check_password(self, password):
        """Compares password against member hash."""
        return werkzeug.security.check_password_hash(self.password_hash,
            password)

    def __repr__(self):
        return f'<Member: {self.name}>'

@app.login.user_loader
def load_user(id):
    return User.query.get(int(id))
