import hashlib
import re

from sqlalchemy import orm, func
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.schema import UniqueConstraint

from warmsea import db


class PropertyError(Exception):
    """Raise when a invalid value is passed to a model property.
    """

    def __init__(self, value, property_name=None, reason=None):
        """Initialize a property error.

        :param value: The (invalid) value.
        :param str property_name: The property name.
        :param str reason: The expect value type (e.g. email).
        """
        self.value = value
        self.property_name = property_name
        self.reason = reason

    def __str__(self):
        """Description of the error.
        """
        desc = '"%s" is not a valid property value' % self.value
        if self.property_name:
            desc += ' for %s' % self.property_name
        if self.reason:
            desc += ': %s' % self.reason
        return desc


class DbExceptionChecker(object):

    def is_duplicate_key(self, ex):
        raise NotImplementedError()


class Option(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False,
                   doc='Option ID')
    key = db.Column(db.String(100), unique=True, nullable=True,
                    doc='Option key')
    value = db.Column(db.Text, nullable=True, default='',
                      doc='Option value')

    @classmethod
    def get(cls, option_key, default_value=''):
        record = cls.query.filter_by(key=option_key).first()
        if record:
            return record.value
        else:
            return default_value

    @classmethod
    def set(cls, option_key, option_value):
        option = cls(key=option_key, value=option_value)
        db.session.merge(option)
        db.session.commit()


class Role(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False,
                   doc='Role ID')
    name = db.Column(db.String(60), nullable=False, unique=True,
                     doc='Role name')
    description = db.Column(db.String(255),
                            doc='Role description')

    def __eq__(self, other):
        return self.id == other.id

    def __lt__(self, other):
        return self.id < other.id

    def __le__(self, other):
        return self.id <= other.id

    def __gt__(self, other):
        return self.id > other.id

    def __ge__(self, other):
        return self.id >= other.id

    def __str__(self):
        return self.name


Role.VISITOR = Role(id=1, name='Visitor',
                    description='Can read posts.')
Role.SUBSCRIBER = Role(id=2, name='Subscriber',
                       description='Can read posts and manage their profile.')
Role.CONTRIBUTOR = Role(id=3, name='Contributor',
                        description='Can write and manage their own posts but cannot publish them.')
Role.AUTHOR = Role(id=4, name='Author',
                   description='Can publish and manage their own posts.')
Role.EDITOR = Role(id=5, name='Editor',
                   description='Can publish and manage posts including the posts of other users.')
Role.ADMINISTRATOR = Role(id=6, name='Administrator',
                          description='Has access to all the administration features.')


class UserStatus(object):
    ACTIVE = 0
    DELETED = 1


class User(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False,
                   doc='User ID')
    _username = db.Column('username', db.String(100), unique=True, nullable=False,
                          doc='Name for signing in')
    _password = db.Column('password', db.String(512), nullable=False,
                          doc='Hashed password')
    role_id = db.Column(db.Integer, db.ForeignKey('role.id'), nullable=False, default=Role.SUBSCRIBER.id,
                        doc='Role ID')
    _nickname = db.Column('nickname', db.String(20), unique=True,
                          doc='Name for displaying')
    avatar = db.Column(db.String(255),
                       doc='User avatar')
    email = db.Column(db.String(150), unique=True,
                      doc='Email address')
    website = db.Column(db.String(255),
                        doc='Website URL')
    status = db.Column('status_id', db.SmallInteger, default=UserStatus.ACTIVE,
                       doc='User status')
    created_at = db.Column(db.TIMESTAMP, default=func.now(),
                           doc='Sign up time')

    role = db.relationship('Role')

    @orm.validates('username')
    def validate_username(self, _, username):
        return username

    @hybrid_property
    def username(self):
        return self._username

    @username.setter
    def username(self, username):
        username = username.strip()
        if len(username) < 3:
            raise Exception('username must be at least 3 characters')
        if not re.match('^[a-zA-Z0-9@_\.-]+$', username):
            raise Exception('username can only contains letters, digits, '
                            'underscores(_), dashes(-), ats(@) and dots(.)')
        self._username = username

    @staticmethod
    def hash_password(password):
        h = hashlib.sha512()
        h.update(password.encode('utf-8'))
        return h.hexdigest()

    @hybrid_property
    def password(self):
        raise PropertyError('User.password is not readable')

    @password.setter
    def password(self, password):
        self._password = self.hash_password(password)

    @hybrid_property
    def nickname(self):
        return self._nickname or self.username

    @nickname.setter
    def nickname(self, nickname):
        if not nickname:
            self._nickname = None
            return
        nickname = nickname.strip()
        if len(nickname):
            self._nickname = nickname
        else:
            self._nickname = None

    @orm.validates('email')
    def validate_email(self, _, email):
        if not email:
            return email
        try:
            new_email = email.strip()
            email_regex = '^[A-Z0-9._%-]+@(?:[A-Z0-9-]+\.)+[A-Z]{2,4}$'
            if re.match(email_regex, new_email, flags=re.IGNORECASE):
                return new_email
            else:
                raise Exception('"%s" is not a valid email' % email)
        except:
            raise Exception('"%s" is not a valid email' % email)

    @orm.validates('status')
    def validate_status(self, _, status):
        if status in [UserStatus.ACTIVE, UserStatus.DELETED]:
            return status
        else:
            raise PropertyError('invalid value for User.status')

    @classmethod
    def verify(cls, username, password):
        """ Verify a user.
        If both the username and password are correct, return the user.

        :param str username: A username.
        :param str password: A password.
        :returns: The user if username and password are correct; otherwise, None.
        :rtype: User or None
        """
        return cls.query.filter_by(username=username, _password=cls.hash_password(password)).first()

    def get_id(self):
        """flask-login requires this method."""
        return self.id

    def is_active(self):
        """flask-login requires this method."""
        return self.status == UserStatus.ACTIVE

    def is_anonymous(self):
        """flask-login requires this method."""
        return False

    def is_authenticated(self):
        """flask-login requires this method."""
        return self.is_active()

    def __str__(self):
        return self.nickname


class UserBinding(db.Model):
    __tablename__ = 'user_binding'

    id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False,
                   doc='Primary key')
    user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False,
                        doc='User ID')
    binding = db.Column(db.String(150), nullable=False, unique=True,
                        doc='Third-part user binding')


class Capability(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False,
                   doc='Capability ID')
    key = db.Column(db.String(100), nullable=False, unique=True,
                    doc='Capability key')

    def __str__(self):
        return self.key


class Category(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False,
                   doc='Category ID')
    name = db.Column(db.String(60), unique=True, nullable=False,
                     doc='Category name')
    _slug = db.Column('slug', db.String(60), unique=True, nullable=False,
                      doc='Category slug')
    description = db.Column(db.String(255),
                            doc='Category description')
    order = db.Column(db.Integer,
                      doc='Category order')

    @orm.validates('name')
    def validate_name(self, _, name):
        name = name.strip()
        if len(name) < 1:
            raise PropertyError(name, 'Category.name is empty')
        return name

    @hybrid_property
    def slug(self):
        return self._slug or self.name

    @slug.setter
    def slug(self, slug):
        slug = slug.strip()
        if len(slug):
            self._slug = slug
        else:
            self._slug = None

    def __str__(self):
        return self.name


class Tag(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False,
                   doc='Tag ID')
    name = db.Column(db.String(60), unique=True, nullable=False,
                     doc='Tag name')
    _slug = db.Column('slug', db.String(60), unique=True, nullable=False,
                      doc='Tag slug')
    order = db.Column(db.Integer,
                      doc='Tag order')

    @orm.validates('name')
    def validate_name(self, _, name):
        name = name.strip()
        if len(name) < 1:
            raise PropertyError(name, 'Tag.name is empty')
        return name

    @hybrid_property
    def slug(self):
        return self._slug or self.name

    @slug.setter
    def slug(self, slug):
        slug = slug.strip()
        if len(slug):
            self._slug = slug
        else:
            self._slug = None

    def __str__(self):
        return self.name


post_tags_table = db.Table('post_tags',
                           db.Column('post_id', db.Integer, db.ForeignKey('post.id')),
                           db.Column('tag_id', db.Integer, db.ForeignKey('tag.id')),
                           UniqueConstraint('post_id', 'tag_id'))


class PostStatus(object):
    ACTIVE = 0
    DELETED = 1
    DRAFT = 2


class PostType(object):
    PAGE = 1
    ARTICLE = 2


class PostFormat(object):
    PLAIN = 1
    MARKDOWN = 2
    HTML = 3


class Post(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False,
                   doc='Post ID')
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False,
                          doc='Author user ID')
    type = db.Column('type_id', db.SmallInteger, index=True, default=PostType.ARTICLE,
                     doc='Post type')
    format = db.Column('format_id', db.SmallInteger, default=PostFormat.MARKDOWN,
                       doc='Post content format')
    title = db.Column(db.String(80),
                      doc='Post title')
    body = orm.deferred(db.Column(db.Text,
                                  doc='Post content body'))
    _excerpt = orm.deferred(db.Column('excerpt', db.Text,
                                      doc='Post excerpt'))
    slug = db.Column(db.String(100), unique=True,
                     doc='Post slug')
    category_id = db.Column(db.Integer, db.ForeignKey('category.id'), index=True,
                            doc='Category ID of the post')
    status = db.Column('status_id', db.SmallInteger, nullable=False, default=PostStatus.ACTIVE,
                       doc='Post status')
    published_at = db.Column(db.TIMESTAMP, nullable=False, index=True, default=func.now(),
                             doc='Publish time')
    created_at = db.Column(db.TIMESTAMP, nullable=False, default=func.now(),
                           doc='Create time')
    updated_at = db.Column(db.TIMESTAMP, nullable=False,
                           default=func.now(), onupdate=func.now(),
                           doc='Last modify time')

    author = db.relationship('User', backref=db.backref('posts', lazy='dynamic'))
    category = db.relationship('Category', backref=db.backref('posts', lazy='dynamic'))
    tags = db.relationship('Tag', secondary=post_tags_table, order_by=Tag.order,
                           backref=db.backref('posts', lazy='dynamic'))

    @hybrid_property
    def excerpt(self):
        return self._excerpt or self.body

    @excerpt.setter
    def excerpt(self, excerpt):
        self._excerpt = excerpt

    def __str__(self):
        if self.title:
            return self.title
        else:
            return self.body[:20] + '...'


class CommentStatus(object):
    ACTIVE = 0
    DELETED = 1


class Comment(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False,
                   doc='Comment ID')
    post_id = db.Column(db.Integer, db.ForeignKey('post.id'), nullable=False,
                        doc='Post ID')

    # If signed in, use author_id.
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False,
                          doc='Comment author user ID')

    # If not signed in, use author_nickname, author_email and author_website.
    # nickname and email are required.
    _author_nickname = db.Column('author_nickname', db.String(20),
                                 doc='Comment author nickname')
    _author_email = db.Column('author_email', db.String(255),
                              doc='Comment author email address')
    _author_website = db.Column('author_website', db.String(255),
                                doc='Comment author website URL')

    body = db.Column(db.Text, nullable=False,
                     doc='Comment content body')
    status = db.Column('status_id', db.SmallInteger, nullable=False, default=CommentStatus.ACTIVE,
                       doc='Comment status')
    parent_id = db.Column(db.Integer, db.ForeignKey('comment.id'),
                          doc='Parent comment ID')
    published_at = db.Column(db.TIMESTAMP, nullable=False, index=True, default=func.now(),
                             doc='Publish time')

    post = db.relationship('Post', backref=db.backref('comments', lazy='dynamic', order_by=published_at))
    author = db.relationship('User')
    children = db.relationship('Comment', backref=db.backref('parent', remote_side=[id]))

    @hybrid_property
    def author_nickname(self):
        if self.author_id:
            return self.author.nickname
        else:
            return self._author_nickname

    @author_nickname.setter
    def author_nickname(self, author_nickname):
        self._author_nickname = author_nickname

    @hybrid_property
    def author_email(self):
        if self.author_id:
            return self.author.email
        else:
            return self._author_email

    @author_email.setter
    def author_email(self, author_email):
        self._author_email = author_email

    @hybrid_property
    def author_website(self):
        if self.author_id:
            return self.author.website
        else:
            return self._author_website

    @author_website.setter
    def author_website(self, author_website):
        self._author_website = author_website

    @property
    def author_avatar(self):
        return ''


class ImageStatus(object):
    ACTIVE = 0
    DELETED = 1
    STORAGE_KEY_RESERVED = 2


class Image(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False,
                   doc='Image ID')
    storage_key = db.Column(db.String(20), unique=True, nullable=False,
                            doc='Image unique key')
    storage = db.Column(db.String(200),
                        doc="Image storage location string")
    author_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False,
                          doc='Author user ID')
    title = db.Column(db.String(80),
                      doc="Image title")
    description = db.Column(db.Text,
                            doc="Image description")
    status = db.Column('status_id', db.SmallInteger, nullable=False, default=ImageStatus.ACTIVE,
                       doc='Image status')
    published_at = db.Column(db.TIMESTAMP, nullable=False, index=True, default=func.now(),
                             doc='Publish time')
    created_at = db.Column(db.TIMESTAMP, nullable=False, default=func.now(),
                           doc='Create time')
    updated_at = db.Column(db.TIMESTAMP, nullable=False,
                           default=(func.now()), onupdate=func.now(),
                           doc='Last modify time')

    author = db.relationship('User', backref=db.backref('images', lazy='dynamic'))


class SrmRound(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False,
                   doc='SRM round ID')
    name = db.Column(db.String(60), nullable=False,
                     doc='SRM round name')
    date = db.Column(db.Date, nullable=False,
                     doc='SRM round date')

    def topcoder_url(self):
        return 'http://community.topcoder.com/stat?c=round_overview&rd=%d' % self.id


class SrmCategory(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False,
                   doc='SRM category ID')
    name = db.Column(db.String(60), unique=True, nullable=False,
                     doc='SRM category name')


srm_round_problems_table = db.Table('srm_round_problems',
                                    db.Column('srm_round_id', db.Integer, db.ForeignKey('srm_round.id')),
                                    db.Column('srm_problem_id', db.Integer, db.ForeignKey('srm_problem.id')),
                                    UniqueConstraint('srm_round_id', 'srm_problem_id'))

srm_problem_categories_table = db.Table('srm_problem_categories',
                                        db.Column('srm_problem_id', db.Integer, db.ForeignKey('srm_problem.id')),
                                        db.Column('srm_category_id', db.Integer, db.ForeignKey('srm_category.id')),
                                        UniqueConstraint('srm_problem_id', 'srm_category_id'))

srm_problem_posts_table = db.Table('srm_problem_posts',
                                   db.Column('srm_problem_id', db.Integer, db.ForeignKey('srm_problem.id')),
                                   db.Column('post_id', db.Integer, db.ForeignKey('post.id')),
                                   UniqueConstraint('srm_problem_id', 'post_id'))


class SrmProblem(db.Model):
    id = db.Column(db.Integer, primary_key=True, autoincrement=True, nullable=False,
                   doc='SRM problem ID')
    name = db.Column(db.String(20), nullable=False,
                     doc='Class name')
    stars = db.Column(db.SmallInteger, nullable=False,
                      doc='Difficulty star')
    note = db.Column(db.Text,
                     doc='Note')

    rounds = db.relationship('SrmRound', secondary=srm_round_problems_table, order_by=SrmRound.date,
                             backref=db.backref('problems', lazy='dynamic'))
    categories = db.relationship('SrmCategory', secondary=srm_problem_categories_table, order_by=SrmCategory.name,
                                 backref=db.backref('problems', lazy='dynamic'))
    related_posts = db.relationship('Post', secondary=srm_problem_posts_table, order_by=Post.id,
                                    backref=db.backref('posts', lazy='dynamic'))

    @hybrid_property
    def topcoder_url(self):
        return u'http://community.topcoder.com/stat?c=problem_statement&pm=%d' % self.id


from .rest_apis import blueprint
