#!python
# This file is placed in the Public Domain.


"main program"


import importlib
import importlib.util
import json
import logging
import os
import os.path
import pathlib
import signal
import sys
import time
import _thread


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


from nixt.clients import NAME, Client, Fleet, spl
from nixt.command import Commands, command, parse, scan
from nixt.handler import Event
from nixt.objects import fmt, update
from nixt.persist import Workdir, j, moddir, pidname, setwd, skel
from nixt.runtime import launch, level, rlog


def out(txt):
    print(txt)
    sys.stdout.flush()


"config"


class Config:

    debug    = False
    default  = "irc,mdl,rss"
    gets     = {}
    init     = ""
    level    = "warn"
    mod      = "mods"
    opts     = {}
    otxt     = ""
    sets     = {}
    verbose  = False
    version  = 411


"clients"


class CLI(Client):

    def __init__(self):
        Client.__init__(self)
        self.register("command", command)

    def raw(self, txt):
        out(txt.encode('utf-8', 'replace').decode("utf-8"))


class Console(CLI):

    def announce(self, txt):
        pass

    def callback(self, event):
        if not event.txt:
            return
        super().callback(event)
        event.wait()

    def poll(self):
        evt = Event()
        evt.txt = input("> ")
        evt.type = "command"
        return evt


"commands"


def cmd(event):
    cmds = sorted(Commands.cmds.keys())
    event.reply(",".join(sorted(cmds)))


def srv(event):
    import getpass
    name = getpass.getuser()
    event.reply(TXT % (NAME.upper(), name, name, name, NAME))


def ver(event):
    mds = ",".join([x.upper() for x in modules(Config.mod)])
    event.reply(f"{NAME.upper()} {Config.version} {mds}")


"daemon"


def daemon(verbose=False):
    pid = os.fork()
    if pid != 0:
        os._exit(0)
    os.setsid()
    pid2 = os.fork()
    if pid2 != 0:
        os._exit(0)
    if not verbose:
        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)


def pidfile(filename):
    if os.path.exists(filename):
        os.unlink(filename)
    path2 = pathlib.Path(filename)
    path2.parent.mkdir(parents=True, exist_ok=True)
    with open(filename, "w", encoding="utf-8") as fds:
        fds.write(str(os.getpid()))


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


def importer(mname, path):
    if path is None:
        path = Config.mod
    module = sys.modules.get(mname, None)
    if module:
        return module
    pth = os.path.join(path, f"{mname}.py")
    if not os.path.exists(pth):
        return 
    spec = importlib.util.spec_from_file_location(mname, pth)
    if not spec:
        return 
    module = importlib.util.module_from_spec(spec)
    sys.modules[mname] = module
    try:
        spec.loader.exec_module(module)
    except Exception as ex:
        logging.exception(ex)
    return module


def inits(names):
    modz = []
    for name in sorted(spl(names)):
        try:
            module = importer(name, Config.mod)
            if not module:
                continue
            if "init" in dir(module):
                thr = launch(module.init)
                modz.append((module, thr))
        except Exception as ex:
            logging.exception(ex)
            _thread.interrupt_main()
    return modz


def scanner(path, names=""):
    res = []
    for nme in sorted(modules(path)):
        if names and nme not in spl(names):
            continue
        module = importer(nme, path)
        scan(module)
        if not module:
            continue
        res.append(module)
    return res


def modules(path):
    if not os.path.exists(path):
        return {}
    return sorted([
            x[:-3] for x in os.listdir(path)
            if x.endswith(".py") and not x.startswith("__")
           ])


"utilities"


def banner():
    tme = time.ctime(time.time()).replace("  ", " ")
    out(f"{NAME.upper()} {Config.version} since {tme} ({Config.level.upper()})")
    mds = modules(Config.mod)
    if mds:
        out(f"loaded {",".join(mds)}")


def check(txt):
    args = sys.argv[1:]
    for arg in args:
        if not arg.startswith("-"):
            continue
        for char in txt:
            if char in arg:
                return True
    return False


def forever():
    while True:
        try:
            time.sleep(0.1)
        except (KeyboardInterrupt, EOFError):
            break


"scripts"


def background():
    daemon("-v" in sys.argv)
    privileges()
    boot(False)
    pidfile(pidname(NAME))
    inits(Config.init or Config.default)
    forever()


def console():
    import readline # noqa: F401
    boot()
    thrs = []
    thrs = inits(Config.init)
    for _mod, thr in thrs:
        if "w" in Config.opts:
            thr.join(30.0)
    csl = Console()
    csl.start(daemon=True)
    forever()


def control():
    if len(sys.argv) == 1:
        return
    boot()
    Commands.add(srv)
    csl = CLI()
    evt = Event()
    evt.orig = repr(csl)
    evt.type = "command"
    evt.txt = Config.otxt
    command(evt)
    evt.wait()


def service():
    privileges()
    boot(False)
    pidfile(pidname(NAME))
    inits(Config.init or Config.default)
    forever()


def boot(doparse=True):
    if doparse:
        parse(Config, " ".join(sys.argv[1:]))
        update(Config, Config.sets)
    level(Config.level)
    if "v" in Config.opts:
        banner()
    rlog("warn", fmt(Config))
    rlog("warn", f"workdir {Workdir.wdr}")
    setwd(NAME)
    scanner(Config.mod)
    Commands.add(cmd)
    Commands.add(ver)


"runtime"


def wrapped(func):
    try:
        func()
    except (KeyboardInterrupt, EOFError):
        out("")
    Fleet.shutdown()


def wrap(func):
    import termios
    old = None
    try:
        old = termios.tcgetattr(sys.stdin.fileno())
    except termios.error:
        pass
    try:
        wrapped(func)
    finally:
        if old:
            termios.tcsetattr(sys.stdin.fileno(), termios.TCSADRAIN, old)


def main():
    if check("v"):
        Config.opts["v"] = True
    if check("a"):
        Config.init = ",".join(modules(Config.mod))
    if check("c"):
        wrap(console)
    elif check("d"):
        background()
    elif check("s"):
        wrapped(service)
    else:
        wrapped(control)


TXT = """[Unit]
Description=%s
After=network-online.target

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

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


if __name__ == "__main__":
    main()
