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


"main"


import json
import logging
import os
import os.path
import pathlib
import signal
import sys
import threading
import time
import _thread


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


from nixt.client import Client, Fleet
from nixt.cmds   import Commands, cmnd, command, scan, table
from nixt.debug  import level
from nixt.disk   import Cache, skel
from nixt.event  import Event
from nixt.parse  import parse
from nixt.paths  import Workdir
from nixt.pkg    import Mods, md5sum, mod, mods, modules
from nixt.thread import launch
from nixt.utils  import spl


"handlers"


def handler(signum, frame):
    out("handler called!")


signal.signal(signal.SIGHUP, handler)


"config"


class Main:

    debug   = False
    ignore  = ["mbx", "mdl", "rst", "udp", "web", "wsd"]
    init    = ""
    level   = "warn"
    md5     = False
    name    = Client.__module__.rsplit(".", maxsplit=1)[0].lower()
    opts    = {}
    verbose = False
    version = 390


"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):
        super().callback(event)
        event.wait()

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


"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 inits(names):
    modz = []
    for name in spl(names):
        try:
            module = mod(name)
            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 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 pidname(name):
    return p(Workdir.wdr, f"{name}.pid")


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


"scripts"


def background():
    daemon("-v" in sys.argv)
    privileges()
    level(Main.level or "debug")
    pidfile(pidname(Main.name))
    table()
    Commands.add(cmd)
    Commands.add(ver)
    inits(Main.init or "irc,rss")
    forever()


def console():
    import readline # noqa: F401
    parse(Main, " ".join(sys.argv[1:]))
    Main.init = Main.sets.get("init", Main.init)
    Main.verbose = Main.sets.get("verbose", Main.verbose)
    Main.level   = Main.sets.get("level", Main.level or "warn")
    level(Main.level)
    table()
    Commands.add(cmd)
    Commands.add(ver)
    if "v" in Main.opts:
        banner()
    for _mod, thr in inits(Main.init):
        if "w" in Main.opts:
            thr.join(30.0)
    csl = Console()
    cmnd(csl, Main.otxt)
    csl.start(daemon=True)
    forever()


def control():
    if len(sys.argv) == 1:
        return
    parse(Main, " ".join(sys.argv[1:]))
    level(Main.level or "warn")
    table()
    Commands.add(cmd)
    Commands.add(md5)
    Commands.add(tbl)
    Commands.add(ver)
    csl = CLI()
    cmnd(csl, Main.otxt)


def service():
    level(Main.level or "warn")
    banner()
    privileges()
    pidfile(pidname(Main.name))
    table()
    Commands.add(cmd)
    Commands.add(ver)
    inits(Main.init or "irc,rss")
    forever()


"commands"


def cmd(event):
    event.reply(",".join(sorted(Commands.names)))


def md5(event):
    table = mods("tbl")[0]
    event.reply(md5sum(table.__file__))


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"""


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


def tbl(event):
    if not check("f"):
        Commands.names = {}
    for mod in mods():
        scan(mod)
    event.reply("# This file is placed in the Public Domain.")
    event.reply("")
    event.reply("")
    event.reply('"lookup tables"')
    event.reply("")
    event.reply("")
    event.reply(f"NAMES = {json.dumps(Commands.names, indent=4, sort_keys=True)}")
    event.reply("")
    event.reply("")
    event.reply("MD5 = {")
    for mod in mods():
        event.reply(f'    "{mod.__name__.split(".")[-1]}": "{md5sum(mod.__file__)}",')
    event.reply("}")

def ver(event):
    event.reply(f"{Main.name.upper()} {Main.version}")


"utilities"


def banner():
    tme = time.ctime(time.time()).replace("  ", " ")
    logging.warning(f"{Main.name.upper()} {Main.version} since {tme} ({Main.level.upper()})")
    mds = modules()
    logging.warning(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


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


"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():
    Mods.ignore = Main.ignore 
    skel()
    if os.path.exists("mods"):
        Mods.path = "mods"
        Mods.pname = "mods"
    if check("a"):
        Main.init = ",".join(modules())
    if check("v"):
        Main.opts["v"] = True
    if check("e"):
        Cache.disk = False
    if check("c"):
        wrap(console)
    elif check("d"):
        background()
    elif check("s"):
        wrapped(service)
    else:
        wrapped(control)


if __name__ == "__main__":
    main()
