from __future__ import annotations
import code
import time
import signal
import parser
import html
import struct
import logging
import random
import math
import uuid
import hashlib
import os
import sys
from telegram import InlineQueryResultArticle, InputTextMessageContent
import re
import ctypes
from . import command
import subprocess
from subprocess import PIPE, STDOUT
import fcntl
import select
from typing import Any, Dict, List, Iterator, Sequence
import typing
import telegram as tg

if typing.TYPE_CHECKING:
    from main import Bea

logger = logging.getLogger("evaluate")

interp = None
safe_interp = None
interp_locals: Dict[str, Any] = {}
python_mode = set()

class Interpreter(code.InteractiveConsole):
    def __init__(self, localVars: Dict[str, Any]) -> None:
        self.cache: List[str] = []
        self.locals: Dict[str, Any]
        self.TRACE_REGEX = re.compile(r"([A-Z][a-z]+Error|[A-Z][a-z]+Exception):?(?:\s+)?(.+)?")
        super().__init__(localVars)

    def write(self, data: str) -> None:
        self.cache.append(data)

    def is_exception(self, data: str) -> bool:
        return True if 'File "<console>", line ' in data else False

    def guru_meditate(self, traceback: str) -> str:
        match = self.TRACE_REGEX.search(traceback)
        if not match:
            return traceback
        exc_name, exc_args = match.groups()
        out = "%s: %s" % (exc_name, exc_args)
        return out

    def flushbuf(self) -> Iterator[str]:
        out = "".join(self.cache).strip()

        if self.is_exception(out):
            # most likely a traceback, only capture exception
            out = self.guru_meditate(out.rsplit("\n", 1)[1])
            raise RuntimeError(out)

        if len(out) > 0:
            for line in out.split('\n'):
                yield line

        self.cache = []

    def run(self, code: str) -> Iterator[str]:
        if not "self" in self.locals.keys():
            self.locals["self"] = self
        self.cache = []
        sys.stdout = sys.stderr = self  # type: ignore
        for line in code.splitlines():
            self.push(line)
        sys.stdout = sys.__stdout__
        sys.stderr = sys.__stderr__
        return self.flushbuf()

@command('pythonmode')
def cmd_pythonmode(bea: Bea, bot: tg.Bot, update: tg.Update) -> None:
    """ Turn on Python mode. In Python mode, all messages you send will be run as Python code. To exit, call exit_pythonmode(update). """
    if update.effective_user:
        python_mode.add(update.effective_user.id)
        bea.reply('🐍')

def exit_pythonmode(update: tg.Update) -> None:
    if update.effective_user:
        python_mode.remove(update.effective_user.id)

def user_in_python_mode(update: tg.Update) -> bool:
    if update.effective_user:
        return update.effective_user.id in python_mode
    return False

@command('^py> (.+)', pass_groups=True, bypass_fn=user_in_python_mode, inline=True, prefix=False)
def cmd_safe_eval(bea: Bea, bot: tg.Bot, update: tg.Update, groups: Sequence[str]) -> None:
    """ Evaluate the given text as Python code, in a sandbox. """
    inline = update.inline_query is not None

    def reply(t):
        if not inline:
            return update.message.reply_text(t, quote=False, parse_mode="HTML")

        results = [InlineQueryResultArticle(
            id=hashlib.sha256(b"py+"+python_code.encode('utf-8')).hexdigest(),
            title=f"Python result: {t.replace('`', '')}",
            input_message_content=InputTextMessageContent(f"`py> {python_code}`\n⇒  `{t}`", parse_mode="Markdown")
        )]

        update.inline_query.answer(results=results, is_personal=True, cache_time=0)

    uid = update.effective_user.id

    if groups:
        python_code = groups[0]
    elif update.effective_message:
        python_code = update.effective_message.text
    elif update.inline_query:
        python_code = update.inline_query.query

    if not python_code: return

    python_code = re.sub(r'(?m)^end$', '\n', python_code.strip())

    try:
        parser.suite(python_code)
    except (SyntaxError, parser.ParserError) as e:
        return reply(f"Error while parsing: {e.args[0]}")

    result = safe_interp.run(python_code)
    if len(result) > 4096:
        result = f"<result too long to show, {len(result)} characters>"
    if not result: return

    return reply(result) if inline else reply(f'<a href="tg://user?id={uid}">{update.effective_user.first_name}</a>: {html.escape(result)}')


@command('^py# (.+)', pass_groups=True, bypass_fn=user_in_python_mode, auth=True, prefix=False)
def cmd_eval(bea: Bea, bot: tg.Bot, update: tg.Update, groups: Sequence[str]) -> None:
    """ Evaluate the given text as Python code, in the Bea process. """
    inline = update.inline_query is not None

    def reply(t):
        if not inline:
            return update.message.reply_text(t, quote=False, parse_mode="HTML")

        results = [InlineQueryResultArticle(
            id=hashlib.sha256(python_code.encode('utf-8')).hexdigest(),
            title=f"Result: {t.replace('`', '')}",
            input_message_content=InputTextMessageContent(f"`{python_code}`\n⇒  `{t}`", parse_mode="Markdown")
        )]

        update.inline_query.answer(results=results, is_personal=True, cache_time=0)

    uid = update.effective_user.id
    interp_locals.update({"update": update, "bea": bea, "bot": bot, "uid": uid})
    if update.message.reply_to_message:
        interp_locals["reply"] = update.message.reply_to_message

    if groups:
        python_code = groups[0]
    elif update.effective_message:
        python_code = update.effective_message.text
    elif update.inline_query:
        python_code = update.inline_query.query

    if not python_code: return

    python_code = re.sub(r'(?m)^end$', '\n', python_code.strip())
    python_code = python_code.replace("‘", "'") \
                             .replace("’", "'") \
                             .replace("“", '"') \
                             .replace("”", '"')

    try:
        parser.suite(python_code)
    except (SyntaxError, parser.ParserError) as e:
        return reply(f"Error while parsing: {e.args[0]}")

    try:
        lines = '\n'.join(interp.run(python_code))
        if not lines.strip(): return
        return reply(lines) if inline else reply(f'<a href="tg://user?id={uid}">{update.effective_user.first_name}</a>: {html.escape(lines)}')
    except RuntimeError as e:
        reply(e.args[0])

class SafeInterpreter:
    def __init__(self) -> None:
        self.process: subprocess.Popen
        self.start_interpreter()

    def start_interpreter(self) -> None:
        if hasattr(self, 'process'):
            self.process.kill()
        self.process = subprocess.Popen(["./interpreter.py"], bufsize=0,
                                        stdin=PIPE, stdout=PIPE, stderr=STDOUT)

        self.pollobj = select.epoll()

        fcntl.fcntl(self.process.stdout, fcntl.F_SETFL, os.O_NONBLOCK)
        self.pollobj.register(self.process.stdout)
        self.pollobj.modify(self.process.stdout, select.EPOLLIN)

    def write(self, text: bytes) -> None:
        self.process.stdin.write(b"ST" + struct.pack("L", len(text)) + text)
        self.process.stdin.flush()

    def read(self, max_bytes: int) -> bytes:
        return self.process.stdout.read(max_bytes)

    def read_response(self) -> str:
        header = self.read_exact(10, timeout=5)
        if not header:
            self.start_interpreter()
            return "<timed out>"

        if (header[:2] != b"ST"):
            self.start_interpreter()
            return "<sandbox crashed or returning invalid results, restarting...>"

        length = struct.unpack("L", header[2:])[0]
        if (length == 0): return ""

        if (length > 2**22):
            self.start_interpreter()
            return "<result far exceeds length, restarting...>"

        result = self.read_exact(length, timeout=5)
        if not result:
            self.start_interpreter()
            return "<timed out>"

        try:
            text_result = result.decode('utf-8')
        except UnicodeDecodeError:
            return "<undecodable text>"
        return text_result

    def read_exact(self, exact_bytes: int, *, timeout: float) -> bytes:
        start = time.time()
        buf = b""
        while True:
            if not (readable := self.wait_until_readable(timeout)):
                return b""
            read = self.read(exact_bytes)
            buf += read
            exact_bytes -= len(read)
            timeout -= time.time() - start

            if (exact_bytes == 0): break
            if (timeout <= 0): return b""
        return buf

    def wait_until_readable(self, timeout: float) -> bool:
        return bool(self.pollobj.poll(timeout))

    def run(self, code: str) -> str:
        if self.process.poll() is not None:
            logger.warning("Python process died? Restarting it...")
            self.start_interpreter()

        code += '\n'
        self.write(code.encode('utf-8'))
    
        return self.read_response()


def init(bea, config_):
    global interp, safe_interp
    if interp is None:
        interp_locals.update(globals())
        interp = Interpreter(interp_locals)
    safe_interp = SafeInterpreter()

    return [cmd_eval, cmd_safe_eval, cmd_pythonmode]
