#!/usr/bin/env python3
# This file is placed in the Public Domain.


"main program"


import argparse
import logging
import os
import sys
import time


sys.path.insert(0, os.getcwd())


from sbn.command import Cfg, Commands
from sbn.clients import Console
from sbn.message import Message
from sbn.package import Mods
from sbn.utility import Utils
from sbn.persist import Workdir
from sbn.utility import Log


from sbn import modules as MODS


"config"


Cfg.commands = True
Cfg.ignore   = "rst,udp,web"
Cfg.level    = "info"
Cfg.modules  = ""
Cfg.name     = Utils.pkgname(Commands)
Cfg.poll     = 300
Cfg.port     = 6667
Cfg.room     = f"#{Cfg.name}"
Cfg.server   = "localhost"
Cfg.txt      = " ".join(sys.argv[1:])
Cfg.version  = 150
Cfg.wdr      = os.path.expanduser(f"~/.{Cfg.name}")


"clients"


class Line(Console):

    def __init__(self):
        super().__init__()
        self.register("command", Commands.command)

    def raw(self, text):
        "write to console."
        print(text.encode('utf-8', 'replace').decode("utf-8"))


class Csl(Line):

    def callback(self, event):
        "wait for callback result."
        if not event.text:
            event.ready()
            return
        super().callback(event)
        event.wait()

    def poll(self):
        "poll for an event."
        evt = Message()
        evt.text = input("> ")
        evt.kind = "command"
        return evt


"scripts"


class Scripts:

    @staticmethod
    def background(args):
        "background script."
        Runtime.daemon()
        Runtime.privileges()
        Runtime.boot(args)
        Workdir.pidfile(Cfg.name)
        Mods.scanner(Mods.list(), Cfg.ignore)
        Commands.add(Cmd.cmd, Cmd.mod, Cmd.ver)
        Mods.inits(Cfg.default or "irc,mdl,rss,wsd", Cfg.ignore)
        Runtime.forever()

    @staticmethod
    def console(args):
        "console script."
        import readline
        readline.redisplay()
        Runtime.boot(args)
        Mods.scanner(Mods.list(), Cfg.ignore)
        Commands.add(Cmd.cmd, Cmd.mod, Cmd.ver)
        Commands.cmd(Cfg.txt)
        Mods.inits(Cfg.modules, Cfg.ignore, Cfg.wait)
        csl = Csl()
        csl.start()
        Runtime.forever()

    @staticmethod
    def control(args):
        "cli script."
        if len(sys.argv) == 1:
            return
        Runtime.boot(args)
        Mods.scanner(Mods.list(), Cfg.ignore)
        Commands.add(Cmd.cmd, Cmd.mod, Cmd.srv, Cmd.ver)
        evt = Commands.cmd(Cfg.txt)
        for line in evt.result.values():
            print(line)

    @staticmethod
    def service(args):
        "service script."
        Runtime.privileges()
        Runtime.banner()
        Runtime.boot(args)
        Workdir.pidfile(Cfg.name)
        Mods.scanner(Mods.list())
        Commands.add(Cmd.cmd, Cmd.mod, Cmd.ver)
        Mods.inits(Cfg.default or "irc,mdl,rss,wsd", Cfg.ignore)
        Runtime.forever()


"commands"


class Cmd:

    @staticmethod
    def cmd(event):
        "list available commands."
        event.reply(",".join(sorted(Commands.names or Commands.cmds)))

    @staticmethod
    def mod(event):
        "list available commands."
        mods = Mods.list(Cfg.ignore)
        if not mods:
            event.reply("no modules available")
            return
        event.reply(mods)

    @staticmethod
    def srv(event):
        "generate systemd service file."
        import getpass
        name = getpass.getuser()
        event.reply(SYSTEMD % (Cfg.name.upper(), name, name, name, Cfg.name))

    @staticmethod
    def ver(event):
        "show verson."
        event.reply(f"{Cfg.name.upper()} {Cfg.version}")


"runtime"


class Runtime:
    
    @staticmethod
    def banner():
        "hello."
        tme = time.ctime(time.time()).replace("  ", " ")
        print("%s %s since %s (%s)" % (
            Cfg.name.upper(),
            Cfg.version,
            tme,
            Cfg.level.upper(),
        ))
        sys.stdout.flush()

    @staticmethod
    def boot(args):
        "in the beginning."
        Cfg.all = args.all
        Cfg.level = args.level
        Cfg.mods = args.mods
        Cfg.port = args.port
        Cfg.room = args.room
        Cfg.server = args.connect
        Cfg.verbose = args.verbose
        Cfg.wait = args.wait
        Cfg.wdr = args.wdr or os.path.expanduser(f"~/.{Utils.pkgname(Cfg)}")
        Log.level(Cfg.level)
        Workdir.setwd(Cfg.wdr)
        if Cfg.wdr:
            Mods.init("modules", os.path.join(Workdir.workdir(), "mods"))
        if MODS:
            Mods.init(MODS.__name__, MODS.__path__[0])
        if Cfg.verbose:
            Runtime.banner()
        if Cfg.all:
            Cfg.modules = Mods.list(Cfg.ignore)

    @staticmethod
    def daemon(verbose=False, nochdir=False):
        "run in the background."
        pid = os.fork()
        if pid != 0:
            os._exit(0)
        os.setsid()
        pid2 = os.fork()
        if pid2 != 0:
            os._exit(0)
        with open('/dev/null', 'r', encoding="utf-8") as sis:
            os.dup2(sis.fileno(), sys.stdin.fileno())
        with open('/dev/null', 'a+', encoding="utf-8") as sos:
            os.dup2(sos.fileno(), sys.stdout.fileno())
        with open('/dev/null', 'a+', encoding="utf-8") as ses:
            os.dup2(ses.fileno(), sys.stderr.fileno())
        os.umask(0)
        os.chdir("/")
        os.nice(10)

    @staticmethod
    def forever():
        "run forever until ctrl-c."
        while True:
            try:
                time.sleep(0.1)
            except (KeyboardInterrupt, EOFError):
                break

    @staticmethod
    def getargs():
        "parse commandline arguments."
        parser = argparse.ArgumentParser(description="Skull, Bones and Number")
        parser.add_argument("-a", "--all", action="store_true", help="load all modules")
        parser.add_argument("-c", "--console", action="store_true", help="start console")
        parser.add_argument("-d", "--daemon", action="store_true", help="start background daemon")
        parser.add_argument("-l", "--level", default=Cfg.level, help='set loglevel')
        parser.add_argument("-m", "--mods", default="", help='modules to load')
        parser.add_argument("-p", "--port", default=Cfg.port, help='set port to connect to')
        parser.add_argument("-r", "--room", help='set room/channel to joinl')
        parser.add_argument("-s", "--service", action="store_true", help="start service")
        parser.add_argument("-v", "--verbose", action='store_true',help='enable verbose')
        parser.add_argument("-w", "--wait", action='store_true',help='wait for services to start')
        parser.add_argument("-z", "--connect", default=Cfg.server, help="server to connect to")
        parser.add_argument("--wdr", help='set working directory')
        return parser.parse_known_args()

    @staticmethod
    def privileges():
        "drop privileges."
        import getpass
        import pwd
        pwnam2 = pwd.getpwnam(getpass.getuser())
        os.setgid(pwnam2.pw_gid)
        os.setuid(pwnam2.pw_uid)

    @staticmethod
    def wrap(func, *args):
        "restore console."
        import termios
        old = None
        try:
            old = termios.tcgetattr(sys.stdin.fileno())
        except termios.error:
            pass
        try:
            func(*args)
        except (KeyboardInterrupt, EOFError):
            pass
        except Exception as ex:
            logging.exception(ex)
        if old:
            termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old)


"data"


SYSTEMD = """[Unit]
Description=%s
After=multi-user.target

[Service]
Type=simple
User=%s
Group=%s
ExecStart=/home/%s/.local/bin/%s -s

[Install]
WantedBy=multi-user.target"""


"runtime"


def main():
    "main"
    args, rest = Runtime.getargs()
    if args.daemon:
        Scripts.background(args)
    elif args.console:
        Runtime.wrap(Scripts.console, args)
    elif args.service:
        Runtime.wrap(Scripts.service, args)
    else:
        Runtime.wrap(Scripts.control, args)
    Mods.shutdown()


if __name__ == "__main__":
    main()
