#!/usr/bin/env python3
from html import escape as htmlescape
import time
import traceback
from telegram.ext import Updater, CommandHandler, MessageHandler, Filters, CallbackQueryHandler, InlineQueryHandler, ChosenInlineResultHandler
from telegram import InlineKeyboardButton, InlineKeyboardMarkup, InlineQueryResultArticle, ParseMode, InputTextMessageContent, ParseMode
from telegram.error import BadRequest
import logging, coloredlogs
from collections import namedtuple
from schema import User, Poll, Choice, Vote, ReferringMessage
from util import logsetup
from uuid import uuid4
from logdata import LogData, InjectTimingAdapter

from pydbus import SessionBus
from gi.repository import GLib

LOGLEVEL = logging.INFO
FORMAT = "[%(asctime)s] [%(levelname)7s] %(name)7s: %(message)s"
lgr = logsetup("microvote.log", FORMAT, LOGLEVEL)
logger = InjectTimingAdapter(lgr, None)

logData = LogData()
states = {}

State = namedtuple("State", ['user', 'name', 'data'])

updater = Updater(token='576534591:AAGZf1QVYkCTLsPKCjWtWfaHkbUjbWkVvmM')
dispatcher = updater.dispatcher
bus = None


def limit(text):
    if len(text) < 20:
        return text
    s = text.split()
    words = ""
    i = 0
    while len(words) < 20:
        words += s[i]
        i += 1
        words += " "
    return words.strip() + "..."


def _u(update):
    if (update.callback_query is not None):
        return update.callback_query.from_user
    return update.message.from_user


def create_user(uid, username):
    User.insert(uid=uid, username=username).on_conflict("IGNORE").execute()


def start(bot, update):
    if (update.message.text.split()[-1] == "newpoll"):
        return newpoll(bot, update)
    update.message.reply_text("""Hey! I'm a bot that does polls!
You can start a new poll with /newpoll.""")


def newpoll(bot, update):
    states[_u(update).id] = State(_u(update), "create_name", None)
    create_user(_u(update).id, _u(update).first_name)
    update.message.reply_text("What will the name of your poll be?")


def publish_poll_to_bus(poll):
    choices = [("/poll/%d/choice/%d" % (poll.id, c.id), DBusChoice(c.id))
               for c in poll.choices]

    try:
        bus.publish("men.meestem.VoteBot",
                    ("/poll/%d" % (poll.id), DBusPoll(poll.id)), *choices)
    except:
        print(poll.id)
        print(choices)
        traceback.print_exc()


def done(bot, update):
    poll = states.get(_u(update).id).data

    del states[_u(update).id]

    update.message.reply_text("Here's your poll!")
    send_poll(update, poll)

    publish_poll_to_bus(poll)

    markup = InlineKeyboardMarkup([[
        InlineKeyboardButton(
            "Share poll", switch_inline_query="%d" % (poll.id))
    ]])

    update.message.reply_text(
        "To share this poll, press the button below.", reply_markup=markup)


def multiple(bot, update):
    poll = states.get(_u(update).id).data
    poll.allow_multiple = True
    update.message.reply_text(
        "Multiple choice has been enabled for this poll.")
    logger.info("Multiple choice set to allowed on poll %s" % (poll.name))
    poll.save()


def cancel(bot, update):
    try:
        poll = states.get(_u(update).id).data
        poll.delete_instance(recursive=True)

        del states[_u(update).id]
    except:
        pass


def send_poll(update, poll):
    poll_markup = get_poll_keyboard(poll)
    message_text = get_poll_message(poll)

    message = update.message.reply_text(
        message_text, reply_markup=poll_markup, parse_mode=ParseMode.HTML)
    rm = ReferringMessage.insert(
        poll=poll,
        chat_id=message.chat_id,
        message_id=message.message_id,
        inline=False).execute()


def get_poll_keyboard(poll):
    poll_buttons = [[
        InlineKeyboardButton(choice.text, callback_data="%d" % choice.id)
    ] for choice in poll.choices]
    poll_markup = InlineKeyboardMarkup(poll_buttons)

    return poll_markup


def get_poll_message(poll):
    lines = ["<b>%s</b>" % poll.name, ""]
    for choice in poll.choices:
        votes = choice.votes
        if (len(votes) > 0):
            lines.append("%s - %d (%s)" % (choice.text, len(votes), ', '.join(
                vote.user.username for vote in votes)))
        else:
            lines.append("%s - %d" % (choice.text, len(votes)))

    return "\n".join(lines)


def update_messages(bot, poll):
    message = get_poll_message(poll)
    markup = get_poll_keyboard(poll)
    logger.info("Updating poll %d (%s) by creator %s.", poll.id,
                limit(poll.name), poll.creator.username)
    for n, referrant in enumerate(poll.referrants):
        logger.info("Updating message %d." % n)
        if referrant.inline:
            try:
                bot.editMessageText(
                    inline_message_id=referrant.message_id,
                    text=message,
                    reply_markup=markup,
                    parse_mode=ParseMode.HTML)
            except BadRequest:
                pass
        else:
            try:
                bot.editMessageText(
                    chat_id=referrant.chat_id,
                    message_id=referrant.message_id,
                    text=message,
                    reply_markup=markup,
                    parse_mode=ParseMode.HTML)
            except BadRequest:
                pass
        logger.info("Updated message %d." % n)


def vote_match_switch(uid, choice, trueFn, falseFn):
    logger.info("Getting matching votes.")
    votes = Vote.select().where((Vote.choice == choice) & (
        Vote.poll == choice.poll) & (Vote.user == uid))

    if len(votes) == 0:
        return falseFn()

    for n, vote in enumerate(votes):
        logger.info("Vote %d." % (n))
        trueFn(vote)
    logger.info("Done getting matching votes.")


def votefor(uid, choice):
    if (choice.poll.allow_multiple):
        logger.info(
            "poll %s allows multiple choice." % limit(choice.poll.name))
        u = User.get(User.uid == uid)
        matched = vote_match_switch(u, choice,
                    lambda vote: vote.delete_instance(),
                    lambda: Vote.insert(choice=choice, poll=choice.poll, user=u).execute())
    else:
        try:
            vote = Vote.get((Vote.poll == choice.poll) &
                            (Vote.user == User.get(User.uid == uid)))
            vote.choice = choice
            vote.save()
        except Vote.DoesNotExist:
            vote = Vote.insert(
                choice=choice,
                poll=choice.poll,
                user=User.get(User.uid == uid)).execute()


def button_press(bot, update):
    logData.tick(update)
    query = update.callback_query
    create_user(_u(update).id, _u(update).first_name)

    choice = Choice.get(Choice.id == int(query.data))

    logger.info("%s pressed %s." % (_u(update).first_name, choice.text))

    votefor(_u(update).id, choice)

    logger.info("started message updates")

    update_messages(bot, choice.poll)

    logger.info("Poll update from %s propagated." % (_u(update).first_name))
    query.answer("You chose %s." % (choice.text))


def poll(bot, update):
    pid = int(update.message.text.split()[-1])

    send_poll(update, Poll.get(Poll.id == pid))


def create_poll(update):
    logger.info("Creating poll for user %d (%s) with poll name %r.",
                _u(update).id,
                _u(update).first_name, update.message.text)
    p = Poll()
    p.name = htmlescape(update.message.text)
    p.creator = User.get(uid=_u(update).id)

    p.save()

    states[_u(update).id] = State(_u(update), "add_choice", p)

    update.message.reply_text(
        "Great! Now send me the choices one-by-one and /done when you're done."
    )


def add_choice(update):
    status = states.get(_u(update).id)
    logger.info("Adding choice %r to poll %d", update.message.text,
                status.data.id)

    c = Choice()
    c.poll = status.data
    c.text = htmlescape(update.message.text)
    c.save()

    update.message.reply_text(
        "Great! Added the choice %r. Add another one, or end with /done." %
        (c.text))


def handle_message(bot, update):
    status = states.get(_u(update).id, State(None, None, None))
    if (status.name == "create_name"):
        create_poll(update)
    if (status.name == "add_choice"):
        add_choice(update)


def get_all_polls(query=""):
    polls = list(Poll.select().order_by(Poll.id.desc()).execute())
    if query:
        polls = [poll for poll in polls if query.lower() in poll.name.lower()]
    results = [
        InlineQueryResultArticle(
            id=poll.id,
            title=poll.name,
            input_message_content=InputTextMessageContent(
                message_text=get_poll_message(poll),
                parse_mode=ParseMode.HTML),
            reply_markup=get_poll_keyboard(poll)) for poll in polls
    ][:8]
    return results


def select_poll(bot, update):
    query = update.inline_query.query

    try:
        query = int(query)
    except:
        return update.inline_query.answer(
            get_all_polls(query),
            cache_time=0,
            switch_pm_text="Create a new poll?",
            switch_pm_parameter="newpoll")

    try:
        poll = Poll.get(Poll.id == int(query))
    except:
        return update.inline_query.answer(
            get_all_polls(),
            cache_time=0,
            switch_pm_text="Create a new poll?",
            switch_pm_parameter="newpoll")

    results = [
        InlineQueryResultArticle(
            id=poll.id,
            title=poll.name,
            input_message_content=InputTextMessageContent(
                message_text=get_poll_message(poll),
                parse_mode=ParseMode.HTML),
            reply_markup=get_poll_keyboard(poll))
    ]

    update.inline_query.answer(
        results,
        cache_time=0,
        switch_pm_text="Create a new poll?",
        switch_pm_parameter="newpoll")


def add_inline_referrant(bot, update):
    poll = Poll.get(Poll.id == update.chosen_inline_result.result_id)
    ReferringMessage.insert(
        poll=poll,
        chat_id=None,
        message_id=update.chosen_inline_result.inline_message_id,
        inline=True).execute()

    logger.info("Adding inline referrant.")


def error_callback(bot, up, error):
    logger.error("Error raised: %s (%s)" % (type(error).__name__,
                                            error.message))


class BotController():
    def sendMessage(self, channel, text):
        updater.bot.send_message(chat_id=channel, text=text)

    def deleteMessage(self, mid, message):
        try:
            bot.editMessageText(inline_message_id=mid, text=message)
        except BadRequest:
            pass
        else:
            bot.editMessageText(chat_id=chat_id, message_id=mid, text=message)

    def updateMessages(self, poll_id):
        poll = Poll.get(Poll.id == poll_id)
        update_messages(updater.bot, poll)

    def addPoll(self, poll_name, choices, username, uid):
        create_user(uid, username)

        poll = Poll()
        poll.name = poll_name
        poll.creator = User.get(uid=uid)
        poll.save()

        for choice in choices:
            c = Choice()
            c.text = choice
            c.poll = poll
            c.save()

        return poll.id


class DBusPoll():
    def __init__(self, poll_id):
        self.poll_id = poll_id

    def _get(self):
        return Poll.get(Poll.id == self.poll_id)

    def deleteMessage(self, mid, message):
        try:
            bot.editMessageText(inline_message_id=mid, text=message)
        except BadRequest:
            pass
        else:
            bot.editMessageText(
                chat_id=referrant.chat_id, message_id=mid, text=message)

    def updateMessages(self):
        update_messages(updater.bot, self._get())
        return True

    def voteFor(self, uid, username, choice_id):
        create_user(uid, username)
        votefor(uid, Choice.get(Choice.id == choice_id))
        self.updateMessages()

    @property
    def name(self):
        return self._get().name

    @name.setter
    def name(self, name):
        p = self._get()
        p.name = name
        p.save()

    @property
    def choices(self):
        return {choice.id: choice.text for choice in self._get().choices}

    @property
    def votes(self):
        return (len(self._get().votes), {
            choice.text: [v.user.username for v in choice.votes]
            for choice in self._get().choices
        })


class DBusChoice():
    def __init__(self, choice_id):
        self.choice_id = choice_id

    def _get(self):
        return Choice.get(Choice.id == self.choice_id)

    @property
    def name(self):
        return self._get().text

    @name.setter
    def name(self, name):
        c = self._get()
        c.text = name
        c.save()

    @property
    def poll(self):
        return "/poll/%d" % (self._get().poll.id)

    @property
    def votes(self):
        return [vote.user.username for vote in self._get().votes]


def loadNodeDescription(name):
    return "".join(open("dbus/%s.xml" % name, "r").readlines())


DBusPoll.dbus = loadNodeDescription("poll")
DBusChoice.dbus = loadNodeDescription("choice")
BotController.dbus = loadNodeDescription("controller")


def main():
    global bus
    loop = GLib.MainLoop()
    bus = SessionBus()

    start_handler = CommandHandler('start', start)
    done_handler = CommandHandler('done', done)
    newpoll_handler = CommandHandler('newpoll', newpoll)
    button_handler = CallbackQueryHandler(button_press)
    allow_multiple_handler = CommandHandler('multiple', multiple)
    ph = CommandHandler('poll', poll)

    msg = MessageHandler(Filters.text, handle_message)
    dispatcher.add_handler(CommandHandler('fullwidth', fullwidth))
    dispatcher.add_handler(allow_multiple_handler)
    dispatcher.add_handler(InlineQueryHandler(select_poll))
    dispatcher.add_handler(CommandHandler('cancel', cancel))
    dispatcher.add_handler(ChosenInlineResultHandler(add_inline_referrant))
    dispatcher.add_handler(msg)
    dispatcher.add_handler(button_handler)
    dispatcher.add_handler(ph)
    dispatcher.add_handler(done_handler)
    dispatcher.add_handler(start_handler)
    dispatcher.add_handler(newpoll_handler)

    dispatcher.add_error_handler(error_callback)

    polls = [("/poll/%d" % p.id, DBusPoll(p.id)) for p in Poll.select()]
    choices = [("/poll/%d/choice/%d" % (p.id, c.id), DBusChoice(c.id))
               for p in Poll.select() for c in p.choices]

    bus.publish("men.meestem.VoteBot", BotController(), *polls, *choices)
    try:
        updater.start_polling()
        loop.run()
    except:
        updater.stop()
        loop.quit()


if __name__ == "__main__":
    main()
