#!/usr/bin/env python3
# -*- coding: utf-8 -*-

__author__ = 'Michael Liao'

' url handlers '

import re, time, json, logging, hashlib, base64, asyncio, datetime, os

import markdown2
import pdb
from aiohttp import web

from coroweb import get, post
from apis import Page, APIValueError, APIResourceNotFoundError

from models import User, Comment, Blog, Category, next_id
from config import configs

COOKIE_NAME = 'awesession'
_COOKIE_KEY = configs.session.secret

def check_admin(request):
    if request.__user__ is None or not request.__user__.admin:
        raise APIPermissionError()

def get_page_index(page_str):
    p = 1
    try:
        p = int(page_str)
    except ValueError as e:
        pass
    if p < 1:
        p = 1
    return p

def user2cookie(user, max_age):
    '''
    Generate cookie str by user.
    '''
    # build cookie string by: id-expires-sha1
    expires = str(int(time.time() + max_age))
    s = '%s-%s-%s-%s' % (user.id, user.passwd, expires, _COOKIE_KEY)
    L = [user.id, expires, hashlib.sha1(s.encode('utf-8')).hexdigest()]
    return '-'.join(L)

def text2html(text):
    lines = map(lambda s: '<p>%s</p>' % s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;'), filter(lambda s: s.strip() != '', text.split('\n')))
    return ''.join(lines)

@asyncio.coroutine
def categorys_bit2tag(categorys_bit):
    '''
    Convert categorys from bit to tag, tag is a list of dict(category_id, category_name, category_checked).
    '''
    categorys_tag = []
    categorys = yield from Category.findAll(orderBy='weight desc')
    for category in categorys:
        # category.id starts from 0
        categorys_tag.append({
            "category_id" : category.id,
            "category_name" : category.name,
            "category_checked" : True if ((categorys_bit >> int(category.id) & 1) == 1) else False
            })
    return categorys_tag

@asyncio.coroutine
def categorys_tag2bit(categorys_tag):
    '''
    Convert categorys from tag to bit.
    '''
    categorys_bit = 0
    for category_tag in categorys_tag:
        if category_tag["category_checked"] == True:
            status = yield from Category.findNumber('count(id)', 'name="%s" and id="%s"' % (category_tag["category_name"], category_tag["category_id"]))
            # verify the status of category record.
            if 1 == status:
                categorys_bit += (1 << int(category_tag["category_id"]))
    return categorys_bit

@asyncio.coroutine
def calcu_relation_num(category_id, hide_ignore = False):
    '''
    Calculate the ralationship number for a given category.
    '''
    relation_num = 0
    blogs = yield from Blog.findAll(orderBy='created_at desc')
    for blog in blogs:
        if (hide_ignore == True) and (blog.hide == True):
            continue
        if 1 == ((blog.categorys_bit >> int(category_id)) & 1):
            relation_num += 1
    return relation_num

@asyncio.coroutine
def cookie2user(cookie_str):
    '''
    Parse cookie and load user if cookie is valid.
    '''
    if not cookie_str:
        return None
    try:
        L = cookie_str.split('-')
        if len(L) != 3:
            return None
        uid, expires, sha1 = L
        if int(expires) < time.time():
            return None
        user = yield from User.find(uid)
        if user is None:
            return None
        s = '%s-%s-%s-%s' % (uid, user.passwd, expires, _COOKIE_KEY)
        if sha1 != hashlib.sha1(s.encode('utf-8')).hexdigest():
            logging.info('invalid sha1')
            return None
        user.passwd = '******'
        return user
    except Exception as e:
        logging.exception(e)
        return None

@get('/')
def index(*, page='1'):
    page_index = get_page_index(page)
    num = yield from Blog.findNumber('count(id)', 'hide=?', [False])
    page = Page(num, page_index)
    if num == 0:
        blogs = []
    else:
        blogs = yield from Blog.findAll('hide=?', [False], orderBy='created_at desc', limit=(page.offset, page.limit))
        for blog in blogs:
            blog.categorys_tag = yield from categorys_bit2tag(blog.categorys_bit)
    categorys = yield from Category.findAll(orderBy='weight desc')
    for category in categorys:
        category.relationNum = (yield from calcu_relation_num(category.id, hide_ignore = True))
    return {
        '__template__': 'blogs.html',
        'page': page,
        'blogs': blogs,
        'categorys': categorys
    }

@get('/blogs/category/{id}')
def get_blogs_in_catgory(*, page='1', id):
    page_index = get_page_index(page)
    try:
        num = (yield from calcu_relation_num(int(id), hide_ignore = True))
    except ValueError as e:
        logging.info('ValueError:', e)
        return 'redirect:/'
    finally:
        pass
    page = Page(num, page_index)
    blogs_in_catgory = []
    if num == 0:
        blogs = []
    else:
        blogs = yield from Blog.findAll('hide=?', [False], orderBy='created_at desc')
        for blog in blogs:
            if ((blog.categorys_bit >> (int)(id)) & 1) == 1:
                blog.categorys_tag = yield from categorys_bit2tag(blog.categorys_bit)
                blogs_in_catgory.append(blog)
    categorys = yield from Category.findAll(orderBy='weight desc')
    for category in categorys:
        category.relationNum = (yield from calcu_relation_num(category.id, hide_ignore = True))
    return {
        '__template__': 'blogs.html',
        'page': page,
        'blogs': blogs_in_catgory[page.offset : (page.offset + page.limit - 1)],
        'categorys': categorys
    }

@get('/blog/{id}')
def get_blog(*, request, id):
    blog = yield from Blog.find(id)
    if blog.hide == True:
        if request.__user__ is None or not request.__user__.admin:
            return 'redirect:/'
    comments = yield from Comment.findAll('blog_id=?', [id], orderBy='created_at desc')
    for c in comments:
        c.html_content = text2html(c.content)
    blog.html_content = markdown2.markdown(blog.content)
    blog.categorys_tag = (yield from categorys_bit2tag(blog.categorys_bit))
    return {
        '__template__': 'blog.html',
        'blog': blog,
        'comments': comments
    }

@get('/register')
def register():
    return {
        '__template__': 'register.html'
    }

@get('/signin')
def signin():
    return {
        '__template__': 'signin.html'
    }

@post('/api/authenticate')
def authenticate(*, email, passwd):
    if not email:
        raise APIValueError('email', 'Invalid email.')
    if not passwd:
        raise APIValueError('passwd', 'Invalid password.')
    users = yield from User.findAll('email=?', [email])
    if len(users) == 0:
        raise APIValueError('email', 'Email not exist.')
    user = users[0]
    # check passwd:
    sha1 = hashlib.sha1()
    sha1.update(user.id.encode('utf-8'))
    sha1.update(b':')
    sha1.update(passwd.encode('utf-8'))
    if user.passwd != sha1.hexdigest():
        raise APIValueError('passwd', 'Invalid password.')
    # authenticate ok, set cookie:
    r = web.Response()
    r.set_cookie(COOKIE_NAME, user2cookie(user, 86400), max_age=86400, httponly=True)
    user.passwd = '******'
    r.content_type = 'application/json'
    r.body = json.dumps(user, ensure_ascii=False).encode('utf-8')
    return r

@get('/signout')
def signout(request):
    referer = request.headers.get('Referer')
    r = web.HTTPFound(referer or '/')
    r.set_cookie(COOKIE_NAME, '-deleted-', max_age=0, httponly=True)
    logging.info('user signed out.')
    return r

@get('/manage/')
def manage():
    return 'redirect:/manage/blogs'

@get('/manage/comments')
def manage_comments(*, page='1'):
    return {
        '__template__': 'manage_comments.html',
        'page_index': get_page_index(page)
    }

@get('/manage/blogs')
def manage_blogs(*, page='1'):
    return {
        '__template__': 'manage_blogs.html',
        'page_index': get_page_index(page)
    }

@get('/manage/blogs/create')
def manage_create_blog():
    return {
        '__template__': 'manage_blog_edit.html',
        'id': '',
        'action': '/api/blogs'
    }

@get('/manage/blogs/edit')
def manage_edit_blog(*, id):
    return {
        '__template__': 'manage_blog_edit.html',
        'id': id,
        'action': '/api/blogs/%s' % id
    }

@get('/manage/categorys')
def manage_categorys():
    return {
        '__template__': 'manage_categorys.html'
    }

@get('/manage/users')
def manage_users(*, page='1'):
    return {
        '__template__': 'manage_users.html',
        'page_index': get_page_index(page)
    }

@get('/api/comments')
def api_comments(*, page='1'):
    page_index = get_page_index(page)
    num = yield from Comment.findNumber('count(id)')
    p = Page(num, page_index)
    if num == 0:
        return dict(page=p, comments=())
    comments = yield from Comment.findAll(orderBy='created_at desc', limit=(p.offset, p.limit))
    return dict(page=p, comments=comments)

@post('/api/blogs/{id}/comments')
def api_create_comment(id, request, *, content):
    user = request.__user__
    if user is None:
        raise APIPermissionError('Please signin first.')
    if not content or not content.strip():
        raise APIValueError('content')
    blog = yield from Blog.find(id)
    if blog is None:
        raise APIResourceNotFoundError('Blog')
    comment = Comment(blog_id=blog.id, user_id=user.id, user_name=user.name, user_image=user.image, content=content.strip())
    yield from comment.save()
    return comment

@post('/api/comments/{id}/delete')
def api_delete_comments(id, request):
    check_admin(request)
    c = yield from Comment.find(id)
    if c is None:
        raise APIResourceNotFoundError('Comment')
    yield from c.remove()
    return dict(id=id)

@get('/api/categorys')
def api_get_categorys(request):
    check_admin(request)
    categorys = yield from Category.findAll(orderBy='weight desc')
    for category in categorys:
        category.relationNum = (yield from calcu_relation_num(category.id))
    return dict(categorys=categorys)

@post('/api/categorys')
def api_add_category(request, *, nameAdded, weightAdded):
    check_admin(request)
    if not nameAdded or not nameAdded.strip():
        raise APIValueError("name", "错误：类别名不应为空，请填写类别名。")
    if not weightAdded:
        raise APIValueError("weight", "错误：类别权重区间为1～100，请设置正确的值。")
    if int(weightAdded) < 1 or int(weightAdded) > 100:
        raise APIValueError("weight", "错误：类别权重区间为1～100，请设置正确的值。")
    sameName = yield from Category.findNumber('count(id)', 'name="%s"' % nameAdded)
    if not 0 == sameName:
        raise APIValueError("name", "错误：已存在同名类别，请使用其他类别名。")
    idAdded = 64
    for i in range(64):
        matchItem = yield from Category.find(i)
        if None == matchItem:
            idAdded = i
            break
    if idAdded == 64:
        raise APIValueError("name", "错误：最多支持64个分类，资源不足。")
    category = Category(id=idAdded, name=nameAdded, weight=weightAdded)
    rows = yield from category.save()
    if not rows == 1:
        raise APIValueError("name", "错误：数据库操作失败。")
    return category

@post('/api/categorys/{id}')
def api_update_categroy(request, *, id, name, weight):
    check_admin(request)
    if not name or not name.strip():
        raise APIValueError("name", "错误：类别名不应为空，请填写类别名。")
    if not weight:
        raise APIValueError("weight", "错误：类别权重区间为1～100，请设置正确的值。")
    if int(weight) < 1 or int(weight) > 100:
        raise APIValueError("weight", "错误：类别权重区间为1～100，请设置正确的值。")
    sameName = yield from Category.findNumber('count(id)', 'name="%s" and id!="%s"' % (name, id))
    if not 0 == sameName:
        raise APIValueError("name", "错误：已存在同名类别，请使用其他类别名。")
    category = Category(id=id, name=name, weight=weight)
    yield from category.update()
    return category

@post('/api/categorys/{id}/delete')
def api_delete_category(request, *, id):
    check_admin(request)
    category = yield from Category.find(id)
    yield from category.remove()
    return dict(id=id)

@get('/api/users')
def api_get_users(*, page='1'):
    page_index = get_page_index(page)
    num = yield from User.findNumber('count(id)')
    p = Page(num, page_index)
    if num == 0:
        return dict(page=p, users=())
    users = yield from User.findAll(orderBy='created_at desc', limit=(p.offset, p.limit))
    for u in users:
        u.passwd = '******'
    return dict(page=p, users=users)

_RE_EMAIL = re.compile(r'^[a-z0-9\.\-\_]+\@[a-z0-9\-\_]+(\.[a-z0-9\-\_]+){1,4}$')
_RE_SHA1 = re.compile(r'^[0-9a-f]{40}$')

@post('/api/users')
def api_register_user(*, email, name, passwd):
    if not name or not name.strip():
        raise APIValueError('name')
    if not email or not _RE_EMAIL.match(email):
        raise APIValueError('email')
    if not passwd or not _RE_SHA1.match(passwd):
        raise APIValueError('passwd')
    users = yield from User.findAll('email=?', [email])
    if len(users) > 0:
        raise APIError('register:failed', 'email', 'Email is already in use.')
    uid = next_id()
    sha1_passwd = '%s:%s' % (uid, passwd)
    user = User(id=uid, name=name.strip(), email=email, passwd=hashlib.sha1(sha1_passwd.encode('utf-8')).hexdigest(), image='http://www.gravatar.com/avatar/%s?d=mm&s=120' % hashlib.md5(email.encode('utf-8')).hexdigest())
    yield from user.save()
    # make session cookie:
    r = web.Response()
    r.set_cookie(COOKIE_NAME, user2cookie(user, 86400), max_age=86400, httponly=True)
    user.passwd = '******'
    r.content_type = 'application/json'
    r.body = json.dumps(user, ensure_ascii=False).encode('utf-8')
    return r

@get('/api/private_blogs')
def api_private_blogs(*, page='1'):
    page_index = get_page_index(page)
    num = yield from Blog.findNumber('count(id)')
    p = Page(num, page_index, page_size=20)
    if num == 0:
        return dict(page=p, blogs=())
    blogs = yield from Blog.findAll(orderBy='created_at desc', limit=(p.offset, p.limit))
    for blog in blogs:
        blog.content = ''
    return dict(page=p, blogs=blogs)


@get('/api/blogs')
def api_blogs(*, page='1'):
    page_index = get_page_index(page)
    num = yield from Blog.findNumber('count(id)','hide=?', [False])
    p = Page(num, page_index)
    if num == 0:
        return dict(page=p, blogs=())
    blogs = yield from Blog.findAll('hide=?', [False], orderBy='created_at desc', limit=(p.offset, p.limit))
    return dict(page=p, blogs=blogs)

@get('/api/blogs/{id}')
def api_get_blog(*, request, id):
    blog = yield from Blog.find(id)
    blog.categorys_tag = yield from categorys_bit2tag(blog.categorys_bit)
    return blog

@get('/api/categorystag')
def api_get_category_tag(*, request):
    categorys_tag = yield from categorys_bit2tag(0)
    return dict(categorys_tag=categorys_tag)

@post('/api/blogs')
def api_create_blog(request, *, name, summary, content, hide, categorys_tag):
    check_admin(request)
    if not name or not name.strip():
        raise APIValueError('name', 'name cannot be empty.')
    if not summary or not summary.strip():
        raise APIValueError('summary', 'summary cannot be empty.')
    if not content or not content.strip():
        raise APIValueError('content', 'content cannot be empty.')
    if not hide:
        hide = False
    if not categorys_tag:
        raise APIValueError('categorys_tag', 'categorys_tag cannot be empty.')
    categorys_bit = yield from categorys_tag2bit(categorys_tag)
    blog = Blog(
        user_id=request.__user__.id,
        user_name=request.__user__.name,
        user_image=request.__user__.image,
        name=name.strip(),
        summary=summary.strip(),
        content=content.strip(),
        hide=hide,
        categorys_bit=categorys_bit
        )
    yield from blog.save()
    return blog

@post('/api/blogs/{id}')
def api_update_blog(id, request, *, name, summary, content, hide, categorys_tag):
    check_admin(request)
    blog = yield from Blog.find(id)
    if not name or not name.strip():
        raise APIValueError('name', 'name cannot be empty.')
    if not summary or not summary.strip():
        raise APIValueError('summary', 'summary cannot be empty.')
    if not content or not content.strip():
        raise APIValueError('content', 'content cannot be empty.')
    if not hide:
        hide = False
    blog.name = name.strip()
    blog.summary = summary.strip()
    blog.content = content.strip()
    blog.hide = hide
    blog.categorys_bit = yield from categorys_tag2bit(categorys_tag)
    yield from blog.update()
    return blog

@post('/api/blogs/{id}/delete')
def api_delete_blog(request, *, id):
    check_admin(request)
    blog = yield from Blog.find(id)
    yield from blog.remove()
    return dict(id=id)

@post('/upload')
def upload_file(*, request):
    data = yield from request.post()
    uploadedFilename = data['files[]'].filename
    logging.info('Uploaded file recevied:', uploadedFilename)
    filecontent = data['files[]'].file.read()
    datetimestr = datetime.datetime.now().strftime("%Y%m%d%H%M%S")
    dirpath = os.path.abspath('files')
    if not os.path.exists(dirpath):
        os.mkdir(dirpath)
    dirpath = os.path.join(dirpath, datetimestr)
    if not os.path.exists(dirpath):
        os.mkdir(dirpath)
    try:
        with open(os.path.join(dirpath, uploadedFilename), 'wb') as f:
            f.write(filecontent)
    except Exception as e:
        logging.warning('File uploaded save failed:', e)
    finally:
        return {'filepath' : os.path.join('/files/', os.path.join(datetimestr, uploadedFilename))}
