#!/usr/bin/env python
# coding: utf-8
#
# Note, the script will daemonize itself unless SYD_BOT_NOBG is set.

import re, os, sys, netrc, shlex, socket, ssl, subprocess, threading, time, uuid
import irc.bot, irc.strings
import certifi, paramiko

MAX_MSG_LENGTH = 370
MAX_MSG_CHUNKS = 3

STFU_TIMEOUT = 60
STFU_INIT = re.compile(r"sydbot[:,]\s+stfu", re.I)
STFU_DONE = re.compile(r"sydbot[:,]\s+done", re.I)
STFU_DEAD = re.compile(r"sydbot[:,]\s+(die|f[su]ck|wtf)", re.I)
STFU_FOOD = re.compile(r"sydbot[:,]\s+botsnack", re.I)
STFU_KILL = re.compile(r"sydbot[:,]\s+poison", re.I)
MSG_OUTER = re.compile(r"\s*<[^>]+>\s*(.+)")
RUST_EVAL = re.compile(r"sydbot[:,]\s*(.+)", re.I)


class SydBot(irc.bot.SingleServerIRCBot):
    def __init__(self, channels, nickname, realname, server, port=6697):
        credentials = netrc.netrc().authenticators(server)
        password = credentials[2] if credentials else None

        contextInstance  = ssl.SSLContext()
        contextInstance.verify_mode = ssl.CERT_REQUIRED
        contextInstance.load_verify_locations(cafile=os.path.relpath(certifi.where()),
                                              capath=None, cadata=None)
        ssl_factory = irc.connection.Factory(wrapper=contextInstance.wrap_socket)
        irc.bot.SingleServerIRCBot.__init__(
            self,
            [(server, port, password)],
            nickname,
            realname,
            connect_factory=ssl_factory,
        )
        self.channel_list = channels
        self.channel_timeouts = {}

    def on_nicknameinuse(self, c, e):
        c.nick(c.get_nickname() + "_")

    def on_welcome(self, c, e):
        for channel in self.channel_list:
            c.join(channel)
            print(f"Joining {channel}", file=sys.stderr)

    def on_privmsg(self, c, e):
        print(f"Received code via privmsg: {e.arguments[0]}", file=sys.stderr)
        self.do_rust(e, e.arguments[0], e.source.nick)

    def on_pubmsg(self, c, e):
        msg = e.arguments[0]
        out = MSG_OUTER.match(msg)
        if out:
            msg = out.group(1)

        if STFU_DONE.match(msg):
            if e.target in self.channel_timeouts:
                del self.channel_timeouts[e.target]
            c.privmsg(e.target, "aye")
            return
        if (
            e.target in self.channel_timeouts
            and time.time() < self.channel_timeouts[e.target]
        ):
            return  # Ignore message if channel is in timeout
        if STFU_DEAD.match(msg):
            c.privmsg(e.target, "No, no, we don't die. Yes, we multiply.")
            return
        elif STFU_FOOD.match(msg):
            cmd = "fortune "
            if "exherbo-dev" in e.target:
                cmd += "exherbo"
            else:
                cmd += "tao-te-ching"
            for msg in subprocess.getoutput(cmd).splitlines():
                msg = msg.strip()
                if msg:
                    while len(msg) > MAX_MSG_LENGTH - 1:
                        c.privmsg(e.target, msg[: MAX_MSG_LENGTH - 1] + "…")
                        time.sleep(0.7)
                        msg = msg[MAX_MSG_LENGTH - 1 :]
                    c.privmsg(e.target, msg)
                else:
                    c.privmsg(e.target, " ")
                time.sleep(0.7)
            return
        elif STFU_INIT.match(msg):
            self.channel_timeouts[e.target] = time.time() + STFU_TIMEOUT
            c.privmsg(e.target, "aye")
            return
        elif STFU_KILL.match(msg):
            c.privmsg(e.target, "You're poison running through my veins!")
            c.privmsg(e.target, "I don't wanna break these chains!")
            return
        elif "rust" in e.target:
            code = RUST_EVAL.match(msg)
            if code is not None:
                code = code.group(1)
                print(f"Received rust code: {code} in {e.target}", file=sys.stderr)
                self.do_rust(e, code, e.target, e.source.nick)
            return  # No terse prefix matches for rust channels.

        cmd = None
        if msg.startswith("; "):
            cmd = msg[2:]
            sh = "rc"
        elif msg.startswith("$ "):
            cmd = msg[2:]
            sh = "bash"
        elif msg.startswith("! "):
            code = msg[2:]
            print(f"Received rust code: {code} in {e.target}", file=sys.stderr)
            self.do_rust(e, code, e.target, e.source.nick)

        if cmd is not None:
            print(
                f"Received command: {cmd} in {e.target} using shell {sh}",
                file=sys.stderr,
            )
            self.do_command(e, msg[1:], e.target, e.source.nick, sh=sh)

    def do_command(self, e, cmd, target, nick, sh="bash"):
        c = self.connection

        if sh == "bash":
            sh = "/bin/bash"
            cmd = shlex.quote(f"({cmd}) 2>&1")
        elif sh == "rc":
            sh = "env PATH=/opt/plan9port/bin:$PATH /opt/plan9port/bin/rc"
            cmd = shlex.quote(f"{{{cmd}}} >[2=1]")
        else:
            raise ValueError(f"Invalid shell {sh}")

        def execute_ssh_command():
            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            ssh.connect("syd.chesswob.org", username="syd", password="syd")

            channel = ssh.get_transport().open_session()
            channel.settimeout(15)
            channel.exec_command(f"{sh} -l -c {cmd}")

            output = ""
            end_time = time.time() + 15  # 15 seconds from now

            while not channel.exit_status_ready():  # Wait for command to complete
                if time.time() > end_time:
                    break
                if channel.recv_ready():
                    output += channel.recv(1024).decode("utf-8", "ignore")
                if channel.recv_stderr_ready():
                    output += channel.recv_stderr(1024).decode("utf-8", "ignore")
                time.sleep(0.1)  # Small delay to prevent high CPU usage

            # Read any remaining output
            if channel.recv_ready():
                output += channel.recv(1024).decode("utf-8", "ignore")
            if channel.recv_stderr_ready():
                output += channel.recv_stderr(1024).decode("utf-8", "ignore")

            ssh.close()
            output = " ".join(
                output.split()
            ).strip()  # Replacing newlines with spaces and removing extra spaces

            if not output:
                output = "<no output>"
            if len(output) > MAX_MSG_LENGTH * MAX_MSG_CHUNKS:
                # Trim the output and add ellipsis if it's longer than the total allowed length
                output = output[: MAX_MSG_LENGTH * MAX_MSG_CHUNKS - 1] + "…"
                # Divide the output into chunks of max_length
                chunks = [
                    output[i : i + MAX_MSG_LENGTH].strip()
                    for i in range(0, len(output), MAX_MSG_LENGTH)
                ]
            else:
                # If the output is within the total allowed length, just divide it into chunks
                chunks = [
                    output[i : i + MAX_MSG_LENGTH].strip()
                    for i in range(0, len(output), MAX_MSG_LENGTH)
                ]

            for chunk in chunks:
                print(f"Sending output '{chunk}' to {nick}@{target}", file=sys.stderr)
                if "exherbo" in target or "sydbox" in target:
                    # SAFETY: We don't want to ping SardemFF7!
                    c.privmsg(target, f"{nick}: {chunk}")
                else:
                    c.notice(target, f"{nick}: {chunk}")

        # Run SSH command in a separate thread to avoid blocking the bot
        thread = threading.Thread(target=execute_ssh_command)
        thread.start()

    def do_rust(self, e, code, target, nick=None):
        c = self.connection

        def execute_rust_code():
            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            ssh.connect("syd.chesswob.org", username="syd", password="syd")

            sftp = ssh.open_sftp()

            fileid = uuid.uuid4().hex
            filename = f"./sydbot_rust_{fileid}.rs"
            executable = f"./sydbot_rust_{fileid}"
            no_main = "main()" not in code
            output = None
            rustok = False
            try:
                with sftp.file(filename, "w") as remote_file:
                    if no_main:
                        remote_file.write("fn main() { println!(\"{:?}\", { ")
                    remote_file.write(code.replace("\\n", "\n"))
                    if no_main:
                        remote_file.write(" }); }")

                compile_cmd = f"rustc -Cdebuginfo=0 -Copt-level=0 -Clto=off -Cpanic=abort {filename} -o {executable} 2>&1"
                stdin, stdout, stderr = ssh.exec_command(compile_cmd, timeout=30)
                compile_output = stdout.read().decode(
                    "utf-8", "ignore"
                ) + stderr.read().decode("utf-8", "ignore")
                output = compile_output.strip()
                rustok = True
            except Exception as e:
                output = "compile error: %r" % e
            finally:
                sftp.close()

            if rustok:
                channel = ssh.get_transport().open_session()
                channel.settimeout(15)
                channel.exec_command(f"/bin/bash -l -c {executable} 2>&1")

                end_time = time.time() + 15  # 15 seconds from now
                while not channel.exit_status_ready():  # Wait for command to complete
                    if time.time() > end_time:
                        break
                    if channel.recv_ready():
                        output += channel.recv(1024).decode("utf-8", "ignore")
                    if channel.recv_stderr_ready():
                        output += channel.recv_stderr(1024).decode("utf-8", "ignore")
                    time.sleep(0.1)  # Small delay to prevent high CPU usage

                # Read any remaining output
                if channel.recv_ready():
                    output += channel.recv(1024).decode("utf-8", "ignore")
                if channel.recv_stderr_ready():
                    output += channel.recv_stderr(1024).decode("utf-8", "ignore")

            ssh.close()
            output = " ".join(output.split()).strip()
            if not output:
                output = "<no output>"
            if len(output) > MAX_MSG_LENGTH * MAX_MSG_CHUNKS:
                output = output[: MAX_MSG_LENGTH * MAX_MSG_CHUNKS - 1] + "…"
            chunks = [
                output[i : i + MAX_MSG_LENGTH].strip()
                for i in range(0, len(output), MAX_MSG_LENGTH)
            ]
            for chunk in chunks:
                if nick is not None:
                    print(f"Sending output '{chunk}' to {nick}@{target}", file=sys.stderr)
                    c.notice(target, f"{nick}: {chunk}")
                else:
                    print(f"Sending output '{chunk}' to {target}", file=sys.stderr)
                    c.privmsg(target, chunk)

        thread = threading.Thread(target=execute_rust_code)
        thread.start()

def daemonize():
    # First fork to detach from parent.
    try:
        pid = os.fork()
        if pid > 0:
            # Exit first parent
            sys.exit(0)
    except OSError as e:
        sys.stderr.write(f"Fork #1 failed: {e.errno} ({e.strerror})\n")
        sys.exit(1)

    # Decouple from parent environment.
    os.chdir("/")
    os.setsid() # Become session leader.
    os.umask(0)

    # Second fork to prevent reacquiring a controlling terminal.
    try:
        pid = os.fork()
        if pid > 0:
            # Exit from second parent.
            sys.exit(0)
    except OSError as e:
        sys.stderr.write(f"Fork #2 failed: {e.errno} ({e.strerror})\n")
        sys.exit(1)

    # Redirect standard file descriptors to /dev/null.
    sys.stdout.flush()
    sys.stderr.flush()
    with open('/dev/null', 'r') as si:
        os.dup2(si.fileno(), sys.stdin.fileno())
    with open('/dev/null', 'a+') as so:
        os.dup2(so.fileno(), sys.stdout.fileno())
    with open('/dev/null', 'a+') as se:
        os.dup2(se.fileno(), sys.stderr.fileno())

def main():
    if len(sys.argv) != 5:
        print("Usage: sydbot <server[:port]> <channel,...> <nickname> <realname>")
        sys.exit(1)

    s = sys.argv[1].split(":", 1)
    server = s[0]
    if len(s) == 2:
        try:
            port = int(s[1])
        except ValueError:
            print("Error: Erroneous port.")
            sys.exit(1)
    else:
        port = 6697
    channels = sys.argv[2].split(",")
    nickname = sys.argv[3]
    realname = sys.argv[4]

    if os.environ.get("SYD_BOT_NOBG", None) is None:
        daemonize()

    bot = SydBot(channels, nickname, realname, server, port)
    bot.start()


if __name__ == "__main__":
    main()
