import logging
import os
import time
from functools import wraps

import delegator
from telegram import InlineKeyboardButton, InlineKeyboardMarkup
from telegram.ext import (CallbackQueryHandler, CommandHandler, Filters,
                          MessageHandler, Updater)
from telegram.ext.dispatcher import run_async

import settings

# Включить ведение журнала
logging.basicConfig(format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
                    level=logging.INFO)

logger = logging.getLogger(__name__)


__tasks = set()


def restricted(func):
    @wraps(func)
    def wrapped(update, context, *args, **kwargs):
        user_id = update.effective_user.id
        if user_id not in settings.ENABLED_USERS:
            print(f"Несанкционированный доступ запрещен для{user_id}.")
            return
        return func(update, context, *args, **kwargs)
    return wrapped


@restricted
def start(update, context):
    def to_buttons(cmd_row):
        return [InlineKeyboardButton(e[0], callback_data=e[1]) for e in cmd_row]
    keyboard = [
        to_buttons(row) for row in settings.SC_MENU_ITEM_ROWS
    ]
    reply_markup = InlineKeyboardMarkup(keyboard)
    msg = ("Любые входные данные будут вызываться как команда оболочки.\r\n"
           "Поддерживаемые команды:\r\n"
           "/script запускать скрипты в ./scripts каталогe\r\n"
           "/tasks показать все запущенные задачи\r\n"
           "/sudo_login вызвать sudo\r\n"
           "/kill убить запущенную задачу\r\n"
           "Shortcut:")
    update.message.reply_text(msg, reply_markup=reply_markup)


def error(update, context):
    """Log Errors caused by Updates."""
    logger.warning('Обновление "%s" вызвало ошибку "%s"', update, context.error)


def __is_out_all(cmd: str) -> (str, bool):
    param = 'oa;'
    if cmd.startswith(param):
        return cmd[len(param):], True
    return cmd, False


@run_async
def __do_exec(cmd, update, context, is_script=False, need_filter_cmd=True):
    message = update.message or update.callback_query.message
    reply_text = message.reply_text  #  func reply_text
    logger.debug('exec command "%s", is_script "%s"', cmd, is_script)

    max_idx = 3
    cmd, is_out_all = __is_out_all(cmd)
    if is_out_all:
        max_idx = 999999

    if need_filter_cmd and not __check_cmd_chars(cmd):
        reply_text(f'Этот cmd является незаконным.')
        return

    if is_script:
        cmd = os.path.join(settings.SCRIPTS_ROOT_PATH, cmd)

    try:
        c = delegator.run(cmd, block=False, timeout=1e6)
    except FileNotFoundError as e:
        reply_text(f"{e}")
        return
    out = ''
    task = (f'{c.pid}', cmd, c)
    __tasks.add(task)
    start_time = time.time()
    idx = 0

    for line in c.subprocess:
        out += line
        cost_time = time.time() - start_time
        if cost_time > 1:
            reply_text(out[:settings.MAX_TASK_OUTPUT])
            idx += 1
            out = ''
            start_time = time.time()
        if idx > max_idx:
            reply_text(f'Команда не завершена. Вы можете убить его, отправив /kill {c.pid}')
            break
    c.block()

    __tasks.remove(task)
    if out:
        reply_text(out[:settings.MAX_TASK_OUTPUT])
    if idx > 3:
        reply_text(f'Задание выполнено: {cmd}')


def __do_cd(update, context):
    cmd: str = update.message.text
    if not cmd.startswith('cd '):
        return False
    try:
        os.chdir(cmd[3:])
        update.message.reply_text(f'pwd: {os.getcwd()}')
    except FileNotFoundError as e:
        update.message.reply_text(f'{e}')
    return True


def __check_cmd(cmd: str):
    cmd = cmd.lower()
    if cmd.startswith('sudo'):
        cmd = cmd[4:].strip()
    cmd = cmd.split(' ')[0]
    if settings.CMD_WHITE_LIST and cmd not in settings.CMD_WHITE_LIST:
        return False
    if cmd in settings.CMD_BLACK_LIST:
        return False
    return True


def __check_cmd_chars(cmd: str):
    for char in settings.CMD_BLACK_CHARS:
        if char in cmd:
            return False
    return True


@restricted
def do_exec(update, context):
    if not update.message:
        return
    if __do_cd(update, context):
        return
    cmd: str = update.message.text
    if not __check_cmd(cmd):
        return
    __do_exec(cmd, update, context)


@restricted
def do_tasks(update, context):
    msg = '\r\n'.join([', '.join(e[:2]) for e in __tasks])
    if not msg:
        msg = "Список задач пуст"
    update.message.reply_text(msg)


@restricted
def do_script(update, context):
    args = context.args.copy()
    if args:
        cmd = ' '.join(args)
        __do_exec(cmd, update, context, is_script=True)
        return
    scripts = '\r\n'.join(
        os.path.join(r[len(settings.SCRIPTS_ROOT_PATH):], file)
        for r, d, f in os.walk(settings.SCRIPTS_ROOT_PATH) for file in f
    )
    msg = "выпонить: /script script_name args\r\n"
    msg += scripts
    update.message.reply_text(msg)


@restricted
def do_kill(update, context):
    if not context.args:
        update.message.reply_text('выпонить: /kill pid')
        return

    pid = context.args[0]
    for task in __tasks:
        if task[0] == pid:
            task[2].kill()
            update.message.reply_text(f'killed: {task[1]}')
            return
    update.message.reply_text(f'pid "{pid}" не найден')


@restricted
def do_sudo_login(update, context):
    if not context.args:
        update.message.reply_text('выпонить: /sudo_login password')
        return

    password = context.args[0]
    c = delegator.chain(f'echo "{password}" | sudo -S xxxvvv')
    out = c.out
    if 'xxxvvv: команда не найдена' in out:
        update.message.reply_text(f'sudo выполнено.')
    update.message.reply_text(f'sudo отказ.')


@restricted
def shortcut_cb(update, context):
    query = update.callback_query
    cmd = query.data
    if cmd not in settings.SC_MENU_ITEM_CMDS.keys():
        update.callback_query.message.reply_text(f'Этот cmd является незаконным.')
    cmd_info = settings.SC_MENU_ITEM_CMDS[cmd]
    is_script = cmd_info[2] if len(cmd_info) >= 3 else False
    __do_exec(cmd, update, context, is_script=is_script, need_filter_cmd=False)


def main():
    updater = Updater(
        settings.TOKEN, use_context=True,
        request_kwargs=settings.REQUEST_KWARGS
    )

    dp = updater.dispatcher

    dp.add_handler(CommandHandler("start", start))
    dp.add_handler(CommandHandler("help", start))
    dp.add_handler(CallbackQueryHandler(shortcut_cb))

    dp.add_handler(CommandHandler("tasks", do_tasks))
    dp.add_handler(CommandHandler("kill", do_kill, pass_args=True))

    if not settings.ONLY_SHORTCUT_CMD:
        dp.add_handler(CommandHandler("sudo_login", do_sudo_login, pass_args=True))
        dp.add_handler(CommandHandler("script", do_script, pass_args=True))
        dp.add_handler(MessageHandler(Filters.text, do_exec))

    dp.add_error_handler(error)
    updater.start_polling()
    logger.info('Telegram shell bot Cicada запущен.')
    updater.idle()


if __name__ == '__main__':
    main()
