#!/usr/bin/env python
# -*- coding: UTF-8 -*-
import logging
import os
import re
from datetime import datetime
import cPickle
import tempfile
from contextlib import closing
from string import Template
import wsgiref.util
import cgi
from urllib import quote_plus, unquote_plus
import hashlib
import random


class RequestContext(object):
    def __init__(self, environ, start_response):
        self.environ = environ
        self.response = start_response
        app_uri = wsgiref.util.application_uri(environ)
        self.app_uri = app_uri if not app_uri.endswith('/') \
            else app_uri[0:len(app_uri) - 1]
        self.path = self.environ.get('PATH_INFO', '/')
        self.method = self.environ['REQUEST_METHOD'].upper()


class Blog(object):
    _statuses = {404: '404 Not Found', 200: '200 OK', 303: '303 See Other',
                 400: '400 Bad Request'}

    _tpl_header = Template('<!DOCTYPE html>\n'
                           '<html xmlns="http://www.w3.org/1999/html">\n'
                           '<head lang="en">\n'
                           '\t<meta charset="${encoding}"/>\n'
                           '\t<link rel="stylesheet" href="${base}/styles.css" '
                           'type="text/css" media="screen"/>\n'
                           '\t<script type="text/javascript" '
                           'src="${base}/script.js"></script>\n'
                           '\t<link type="application/atom+xml" rel="alternate"'
                           ' title="${title}" href="${feed_url}" />\n'
                           '\t<title>${title}</title>\n'
                           '</head>\n'
                           '<body${body_tag}>\n'
                           '\t<header>\n'
                           '\t\t<h1>${title}</h1>\n'
                           '\t</header>\n'
                           '\t<main>\n')

    _tpl_link = Template('<a href="${link}">${title}</a>')
    _tpl_link_wth_cls = Template('<a href="${link}" class="${cls}">${title}'
                                 '</a>')

    _tpl_entries_begin = '\t\t<section>\n'
    _tpl_entry = Template('\t\t\t<article>\n'
                          '\t\t\t<header>\n'
                          '\t\t\t\t<h2>${title}</h2></header>\n'
                          '\t\t\t<time>${time}</time>\n'
                          '\t\t\t<p>${text}</p>\n'
                          '\t\t\t<footer>\n'
                          '\t\t\t\t<div>posted in ${categories}</div>\n'
                          '\t\t\t\t<div>${comments}</div>\n'
                          '\t\t\t</footer>\n'
                          '\t\t\t</article>\n')
    _tpl_view_full = Template('<a href="#">View full post &rarr;</a>')
    _tpl_entries_end = '\t\t</section>\n'

    _tpl_aside = Template('\t\t<aside>\n'
                          '\t\t\t<nav><h2>Categories</h2>\n'
                          '\t\t\t\t<ul>\n'
                          '${categories}'
                          '\t\t\t\t</ul>\n'
                          '\t\t\t</nav>\n'
                          '\t\t\t<nav><h2>Archive</h2>\n'
                          '\t\t\t\t<ul>\n'
                          '${archive}'
                          '\t\t\t\t</ul>\n'
                          '\t\t\t</nav>\n'
                          '\t\t</aside>\n')

    _tpl_aside_entry = Template('\t\t\t\t\t<li><a href="${link}">${title}</a>'
                                '</li>\n')

    _tpl_footer = Template('\t</main>\n'
                           '\t<footer>\n'
                           '\t\t<nav>${links}</nav>\n'
                           '\t</footer>\n'
                           '</body>\n'
                           '</html>\n')

    _tpl_post = Template('\t\t\t<article>\n'
                         '\t\t\t\t<header><h2>${title}</h2></header>\n'
                         '\t\t\t\t<time>${time}</time>\n'
                         '\t\t\t\t<p>${text}</p>\n'
                         '\t\t\t\t<footer>\n'
                         '\t\t\t\t<div>posted in ${categories}</div>\n'
                         '\t\t\t\t<div class="comments" id="comments">\n'
                         '\t\t\t\t<header><h3>${comments_title}</h3>'
                         '<a onclick="toggleReplyForm(\'reply-form\');'
                         'return false;" href="#comments">Reply</a></header>\n'
                         '\t\t\t\t<div class="reply-wrapper" id="reply-form" '
                         'style="display:none;">\n'
                         '\t\t\t\t<form method="post" class="reply-form"'
                         'action="${reply_url}">\n'
                         '\t\t\t\t<div><input type="text" class="cobweb" '
                         'name="cobweb" placeholder="Please, paste ${token} '
                         'value into this field" value=""/></div>\n'
                         '\t\t\t\t<div><input name="email" '
                         'type="text" placeholder="Email" value=""/></div>\n'
                         '\t\t\t\t<div><input name="name" '
                         'type="text" placeholder="Name" value=""/></div>\n'
                         '\t\t\t\t<div><textarea rows="4" placeholder="Comment"'
                         ' name="comment"></textarea></div>\n'
                         '\t\t\t\t<div><input type="submit" value="Send"/>'
                         '</div>\n'
                         '\t\t\t\t</form>\n'
                         '\t\t\t\t</div>\n'
                         '\t\t\t\t\t${comments}\n'
                         '\t\t\t\t</div>\n'
                         '\t\t\t\t</footer>\n'
                         '\t\t\t</article>\n')

    _tpl_comment = Template('\t\t\t\t<div class="comment">\n'
                            '\t\t\t\t<div class="comment_body">\n'
                            '\t\t\t\t<header><h3>${name}</h3>'
                            '<time>${time}</time><span class="delete">'
                            '${delete_url}</span></header>\n'
                            '\t\t\t\t<p>${comment}</p>\n'
                            '\t\t\t\t<footer><a href="#" '
                            'onclick="toggleReplyForm(\'reply-form-${id}\');'
                            'return false;">Reply</a></footer>\n'
                            '\t\t\t\t<div class="reply-wrapper" '
                            'id="reply-form-${id}" style="display:none;">\n'
                            '\t\t\t\t<form method="post" class="reply-form"'
                            'action="${reply_url}">\n'
                            '\t\t\t\t<div><input type="text" class="cobweb" '
                            'name="cobweb" placeholder="Please, paste ${token} '
                            'value into this field" value=""/></div>\n'
                            '\t\t\t\t<input name="comment_no" type="hidden" '
                            'value="${id}"/>\n'
                            '\t\t\t\t<div><input name="email" '
                            'type="text" placeholder="Email" value=""/></div>\n'
                            '\t\t\t\t<div><input name="name" '
                            'type="text" placeholder="Name" value=""/></div>\n'
                            '\t\t\t\t<div><textarea rows="4" '
                            'placeholder="Comment" name="comment"></textarea>'
                            '</div>\n'
                            '\t\t\t\t<div><input type="submit" value="Send"/>'
                            '</div>\n'
                            '\t\t\t\t</form>\n'
                            '\t\t\t\t</div>\n'
                            '\t\t\t\t</div>\n'
                            '\t\t\t\t<div class="reply_comments">${comments}'
                            '</div>\n'
                            '\t\t\t\t</div>\n')

    _tpl_delete_comment = Template('\t\t\t<form method="post" '
                                   'action="${url}/${ids}">\n'
                                   '\t\t\t\t<input name="password" '
                                   'type="password" placeholder="Password"/>\n'
                                   '\t\t\t\t<input type="submit" '
                                   'value="Delete"/>\n'
                                   '\t\t\t</form>\n')

    _tpl_feed_begin = Template('<?xml version="1.0" encoding="${encoding}"?>\n'
                               '<feed xmlns="http://www.w3.org/2005/Atom">\n'
                               '\t<title>${title}</title>\n'
                               '\t<link rel="self" type="text/xml" '
                               'href="${self_url}"/>\n'
                               '\t<link type="text/html" rel="alternate" '
                               'href="${url}"/>\n'
                               '\t<updated>${updated}</updated>\n'
                               '\t<author><name>${author}</name></author>\n'
                               '\t<id>urn:${id}</id>\n')

    _tpl_feed_entry = Template('\t<entry>\n'
                               '\t\t<id>urn:${id}</id>\n'
                               '\t\t<title>${title}</title>\n'
                               '\t\t<link type="text/html" rel="alternate" '
                               'href="${url}"/>\n'
                               '${categories}'
                               '\t\t<updated>${updated}</updated>\n'
                               '\t\t<content type="text/html">${content}'
                               '</content>\n'
                               '\t</entry>\n')

    _tpl_feed_category = Template('\t\t<category term="${category}"/>\n')

    _tpl_feed_end = '</feed>'

    def __init__(self):
        self._encoding = 'UTF-8'
        script_path, _ = os.path.split(os.path.realpath(__file__))
        conf = dict()
        conf_path = os.path.join(script_path, 'index.conf')
        try:
            execfile(conf_path, conf)
        except IOError:
            print 'I wasn\'t able to read configuration file [%s]. Default ' \
                  'settings will be used' % conf_path
        logging.basicConfig(level=logging.DEBUG)
        self._logger = logging.getLogger(__name__)
        self.entries_dir = conf.get('entries_path', os.path.join(script_path,
                                                                 'entries'))
        self.indices_dir = conf.get('indices_path', os.path.join(script_path,
                                                                 'indices'))
        self.comments_dir = conf.get('comments_path', os.path.join(script_path,
                                                                   'comments'))
        self.file_name_sep = conf.get('file_name_separator', '-')
        self.title = conf.get('title', '')
        try:
            self.items_per_page = int(conf.get('items_per_page', 7))
        except ValueError:
            self.items_per_page = 7
        try:
            self.items_per_feed = int(conf.get('items_per_feed', 7))
        except ValueError:
            self.items_per_feed = 7
        try:
            self.comments_nesting = int(conf.get('comments_nesting', 7))
        except ValueError:
            self.comments_nesting = 7
        self.index = self._try_main_index(os.path.join(self.indices_dir,
                                                       'main.index'))
        self.author = conf.get('author', 'anonymous')
        self.categories = self.list_categories()
        self.archive = self.list_archive()
        password = conf.get('password')
        if password:
            m = hashlib.md5()
            m.update(password)
            self.password = m.digest()
        else:
            self.password = None
        self._salt = conf.get('salt', ''.join(random.choice('0123456789ABCDEF')
                                              for _ in range(16)))

    def _serialize_object(self, obj, file_path, force=False):
        tmp_fd, tmp_path = tempfile.mkstemp(dir=self.indices_dir)
        with closing(os.fdopen(tmp_fd, 'wb')) as tmp_file:
            cPickle.dump(obj, tmp_file, protocol=cPickle.HIGHEST_PROTOCOL)
        try:
            if not os.path.exists(file_path) or force:
                os.rename(tmp_path, file_path)
                self._logger.debug('an object was serialized into file [%s]',
                                   file_path)
        finally:
            if os.path.exists(tmp_path):
                os.remove(tmp_path)

    def _try_main_index(self, main_index_path):
        if not os.path.exists(main_index_path):
            return self._create_main_index(main_index_path)
        else:
            with open(main_index_path, 'rb') as f:
                return cPickle.load(f)

    def _create_main_index(self, main_index_path):
        entries = list()
        re_file_name = re.compile('^(.+)' + self.file_name_sep +
                                  '(\d{4}-\d{2}-\d{2})\.txt$')
        for file_name in os.listdir(self.entries_dir):
            if os.path.isfile(os.path.join(self.entries_dir, file_name)):
                matched = re_file_name.match(file_name)
                if matched:
                    try:
                        date = datetime.strptime(matched.group(2), '%Y-%m-%d')
                        entries.append((date, matched.group(1),
                                        self._read_categories(file_name)))
                    except ValueError:
                        continue
        entries.sort(reverse=True, key=lambda entry: (entry[0], entry[1]))
        self._serialize_object(entries, main_index_path)
        return entries

    def _read_categories(self, file_name):
        re_categories = re.compile('^categories:([^,]+)(?:,([^,]+))*$')
        categories = set()
        with open(os.path.join(self.entries_dir, file_name), mode='r') as f:
            for line in f:
                matched = re_categories.match(line)
                if matched:
                    for category in matched.groups():
                        categories.add(category.strip())
                    break
        return categories

    def status(self, rc, code, response):
        rc.response(self._statuses[code], [('Content-Type',
                                            'text/plain; charset=%s' %
                                            self._encoding)])
        return response

    def redirect(self, rc, url):
        rc.response(self._statuses[303], [('Location', rc.app_uri + url)])

    def list_file_names(self):
        main_index_path = os.path.join(self.indices_dir, 'main.index')
        if not os.path.exists(main_index_path):
            self._create_main_index(main_index_path)
        with open(main_index_path, 'rb') as f:
            return cPickle.load(f)

    def filter_entries(self, category, archive):
        if category:
            return [entry for entry in self.index if category in entry[2]]
        elif archive:
            archive = datetime.strptime(archive, '%Y-%m')
            return [entry for entry in self.index if (archive.year,
                                                      archive.month) ==
                                                     (entry[0].year,
                                                      entry[0].month)]
        return self.index

    def read_post(self, entry):
        date, pid, cats = entry
        post = dict()
        post['date'] = date
        post['id'] = pid
        with open(os.path.join(self.entries_dir,
                               self.build_file_name(entry))) as f:
            preview, full = False, False
            for line in f:
                if line.startswith('categories:') and 'categories' not in post:
                    post['categories'] = [category.strip() for category in
                                          line.lstrip('categories:').split(",")]
                    preview, full = False, False
                elif line.startswith('title:') and 'title' not in post:
                    post['title'] = line.lstrip('title:').strip()
                    preview, full = False, False
                elif line.startswith('preview:') and not preview \
                        and 'preview' not in post:
                    preview, full = True, False
                    post['preview'] = line.lstrip('preview:').lstrip()
                elif line.startswith('full:') and not full \
                        and 'full' not in post:
                    preview, full = False, True
                    post['full'] = line.lstrip('full:').lstrip()
                elif preview:
                    post['preview'] += line
                elif full:
                    post['full'] += line
        if 'categories' not in post:
            post['categories'] = []
        return post

    def list_archive(self):
        archive = list()
        year, month = None, None
        for date, _, _ in self.index:
            if (date.year, date.month) != (year, month):
                archive.append(str(date.year) + "-" + str(date.month))
                year, month = date.year, date.month
        return archive

    def list_categories(self):
        categories = set()
        for _, _, cats in self.index:
            categories.update(cats)
        categories = list(categories)
        categories.sort()
        return categories

    @staticmethod
    def build_base_uri(app_uri, category, archive, page):
        uri = app_uri
        if category:
            uri += '/category/' + category
        elif archive:
            uri += '/archive/' + archive
        if page:
            uri += '/page/' + str(page)
        return uri

    def build_file_name(self, entry):
        date, pid, _ = entry
        return pid + self.file_name_sep + date.strftime('%Y-%m-%d') + '.txt'

    def find_entry(self, archive, pid):
        try:
            date = datetime.strptime(archive, '%Y-%m-%d')
            entry = next(entry for entry in self.index
                         if (date, pid) == entry[0:2])
            return entry if entry else None
        except ValueError:
            return None

    def get_comment(self, comments, comments_num):
        comment = None
        level = 0
        while comments_num:
            index = comments_num[0]
            if level == self.comments_nesting:
                return comment
            if index < len(comments):
                comment = comments[index]
                comments, comments_num = comment[4], comments_num[1:]
            else:
                comment, comments_num = None, None
            level += 1
        return comment

    def gather_comments(self, app_uri, comments, archive, pid, token, admin):
        reply_url = app_uri + '/post/' + archive + '/' + pid
        delete_url = app_uri + '/delete/' + archive + '/' + pid