#
# Required  privileges: ALTPRI, SYSNAM, PRMMBX, IMPERSONATE, SETPRV, WORLD
#
import argparse
import configparser
import ctypes
import datetime
import json
import logging
import logging.handlers
import queue
from dataclasses import dataclass, field
from enum import IntEnum
from typing import Any, Dict, List, Literal, Tuple, Type, NewType

from ovms import (
    accdef,
    cmbdef,
    crtl,
    dvidef,
    iodef,
    iosbdef,
    jpidef,
    lnmdef,
    mbxqio,
    pqldef,
    prcdef,
    prvdef,
    psldef,
    ssdef,
    starlet,
    stsdef,
)
from ovms import ast as vmsast
from ovms.rtl import lib

logger: logging.Logger
logger = logging.getLogger('supervisord')

supervisord_table_name = b'SUPERVISORD_TABLE'
current_tick: int = 0
timer_queue: queue.PriorityQueue['PrioritizedItem'] = queue.PriorityQueue()

supervisorctl_pwd = ''

TRUTHY_STRINGS = ('yes', 'true', 'on', '1')
FALSY_STRINGS = ('no', 'false', 'off', '0')

timer_delay = starlet.bintim('0 0:00:01.00')[1]
timer_astctx = vmsast.AstContext(vmsast.M_WAKE | vmsast.M_QUEUE)


# Define a new type called "PrvMask", which is internally an `int`
PrvMask = NewType('PrvMask', int)
# Ditto for "PidType"
PidType = NewType('PidType', int)

privileges: Dict[str, PrvMask] = {
    'NOSAME': PrvMask(0),
    'ACNT': PrvMask(prvdef.PRV_M_ACNT),
    'ALLSPOOL': PrvMask(prvdef.PRV_M_ALLSPOOL),
    'ALTPRI': PrvMask(prvdef.PRV_M_ALTPRI),
    'AUDIT': PrvMask(prvdef.PRV_M_AUDIT),
    'BUGCHK': PrvMask(prvdef.PRV_M_BUGCHK),
    'BYPASS': PrvMask(prvdef.PRV_M_BYPASS),
    'CMEXEC': PrvMask(prvdef.PRV_M_CMEXEC),
    'CMKRNL': PrvMask(prvdef.PRV_M_CMKRNL),
    'DIAGNOSE': PrvMask(prvdef.PRV_M_DIAGNOSE),
    'DOWNGRADE': PrvMask(prvdef.PRV_M_DOWNGRADE),
    'EXQUOTA': PrvMask(prvdef.PRV_M_EXQUOTA),
    'GROUP': PrvMask(prvdef.PRV_M_GROUP),
    'GRPNAM': PrvMask(prvdef.PRV_M_GRPNAM),
    'GRPPRV': PrvMask(prvdef.PRV_M_GRPPRV),
    'IMPERSONATE': PrvMask(prvdef.PRV_M_IMPERSONATE),
    'IMPORT': PrvMask(prvdef.PRV_M_IMPORT),
    'LOG_IO': PrvMask(prvdef.PRV_M_LOG_IO),
    'MOUNT': PrvMask(prvdef.PRV_M_MOUNT),
    'NETMBX': PrvMask(prvdef.PRV_M_NETMBX),
    'OPER': PrvMask(prvdef.PRV_M_OPER),
    'PFNMAP': PrvMask(prvdef.PRV_M_PFNMAP),
    'PHY_IO': PrvMask(prvdef.PRV_M_PHY_IO),
    'PRMCEB': PrvMask(prvdef.PRV_M_PRMCEB),
    'PRMGBL': PrvMask(prvdef.PRV_M_SYSGBL),
    'PRMMBX': PrvMask(prvdef.PRV_M_PRMMBX),
    'PSWAPM': PrvMask(prvdef.PRV_M_PSWAPM),
    'READALL': PrvMask(prvdef.PRV_M_READALL),
    'SECURITY': PrvMask(prvdef.PRV_M_SECURITY),
    'SETPRV': PrvMask(prvdef.PRV_M_SETPRV),
    'SHARE': PrvMask(prvdef.PRV_M_SHARE),
    'SHMEM': PrvMask(prvdef.PRV_M_SHMEM),
    'SYSGBL': PrvMask(prvdef.PRV_M_SYSGBL),
    'SYSLCK': PrvMask(prvdef.PRV_M_SYSLCK),
    'SYSNAM': PrvMask(prvdef.PRV_M_SYSNAM),
    'SYSPRV': PrvMask(prvdef.PRV_M_SYSPRV),
    'TMPMBX': PrvMask(prvdef.PRV_M_TMPMBX),
    'UPGRADE': PrvMask(prvdef.PRV_M_UPGRADE),
    'VOLPRO': PrvMask(prvdef.PRV_M_VOLPRO),
    'WORLD': PrvMask(prvdef.PRV_M_WORLD),
}


quotas: Dict[str, int] = {
    'ASTLM': pqldef.PQL__ASTLM,
    'BIOLM': pqldef.PQL__BIOLM,
    'BYTLM': pqldef.PQL__BYTLM,
    'CPULM': pqldef.PQL__CPULM,
    'DIOLM': pqldef.PQL__DIOLM,
    'ENQLM': pqldef.PQL__ENQLM,
    'FILLM': pqldef.PQL__FILLM,
    'JTQUOTA': pqldef.PQL__JTQUOTA,
    'PGFLQUOTA': pqldef.PQL__PGFLQUOTA,
    'PRCLM': pqldef.PQL__PRCLM,
    'TQELM': pqldef.PQL__TQELM,
    'WSDEFAULT': pqldef.PQL__WSDEFAULT,
    'WSEXTENT': pqldef.PQL__WSEXTENT,
    'WSQUOTA': pqldef.PQL__WSQUOTA,
}


def start_timer():
    global timer_delay, timer_astctx
    timer_astctx.reset()
    starlet.setimr(daytim=timer_delay, reqidt=timer_astctx)


class RestartWhenExitUnexpected:
    pass


class RestartUnconditionally:
    pass


def auto_restart(
    value: str,
) -> (
    Type[RestartUnconditionally]
    | Type[RestartWhenExitUnexpected]
    | Literal[False]
):
    value = str(value.lower())
    computed_value = value
    if value in TRUTHY_STRINGS:
        computed_value = RestartUnconditionally
    elif value in FALSY_STRINGS:
        computed_value = False
    elif value == 'unexpected':
        computed_value = RestartWhenExitUnexpected
    if computed_value not in (
        RestartWhenExitUnexpected,
        RestartUnconditionally,
        False,
    ):
        raise ValueError("invalid 'autorestart' value %r" % value)
    return computed_value


class TimerItemType(IntEnum):
    UNDEF = 0
    PROC_STARTING = 1
    PROC_BACKOFF = 2
    PROC_STOPPING = 3


class TimerItem(object):
    typ: TimerItemType
    value: Any
    cancel: bool
    done: bool

    def __init__(
        self,
        typ: TimerItemType = TimerItemType.UNDEF,
        value: Any = None,
    ) -> None:
        self.typ = typ
        self.value = value
        self.cancel = False
        self.done = False


@dataclass(order=True)
class PrioritizedItem:
    priority: int
    item: TimerItem = field(compare=False)


class AstParamType(IntEnum):
    PROCTERM = 1
    CMD = 2
    TIMER = 3


# http://supervisord.org/subprocess.html#process-states
class ProcessStates(IntEnum):
    STOPPED = 0
    STARTING = 10
    RUNNING = 20
    BACKOFF = 30
    STOPPING = 40
    EXITED = 100
    FATAL = 200
    UNKNOWN = 1000


class ProcessInfo(object):
    pid: PidType
    state: ProcessStates
    finalsts: int | None
    start_time: datetime.datetime | None
    end_time: datetime.datetime | None

    def __init__(
        self,
        pid: PidType = PidType(0),
        start_time: datetime.datetime | None = None,
        state: ProcessStates = ProcessStates.STOPPED,
    ) -> None:
        self.pid = pid
        self.start_time = start_time
        self.end_time = None
        self.finalsts = None
        self.state = state


class Program(object):
    mbxunt = 0
    name: str
    image: bytes
    command: bytes
    program_name: bytes
    autostart: bool
    priority: int
    stdout_logfile: bytes
    stderr_logfile: bytes
    startsecs: int
    startretries: int
    stopwaitsecs: int
    kt_limit: int
    baspri: int
    nouaf: bool
    quotas: List[Tuple[int, int]] | None
    prv: PrvMask | None
    user: bytes
    process: ProcessInfo
    running_processes: Dict[PidType, 'Program']
    running_processes = dict()
    programs: Dict[str, 'Program']
    programs = dict()
    remain_startretries: int
    timer_item = TimerItem | None
    kill_request: bool
    autorestart: (
        Type[RestartUnconditionally]
        | Type[RestartWhenExitUnexpected]
        | Literal[False]
    )
    exitcodes: List[int]

    def __init__(
        self,
        name: str,
        user: str,
        command: str,
        image: str,
        process_name: str,
        autostart: bool,
        priority: int,
        stdout_file: str,
        stderr_file: str,
        startsecs: int,
        startretries: int,
        stopwaitsecs: int,
        nouaf: bool,
        quotas: List[Tuple[int, int]] | None,
        prv: PrvMask | None,
        kt_limit: int,
        baspri: int,
        autorestart: (
            Type[RestartUnconditionally]
            | Type[RestartWhenExitUnexpected]
            | Literal[False]
        ),
        exitcodes: List[int],
    ):
        self.name = name
        self.user = user.encode()
        self.image = image.encode()
        self.command = command.encode()
        self.process_name = process_name.encode()
        self.autostart = autostart
        self.priority = priority
        self.stdout_file = stdout_file.encode()
        self.stderr_file = stderr_file.encode()
        self.startsecs = startsecs
        self.startretries = startretries
        self.remain_startretries = startretries + 1
        self.stopwaitsecs = stopwaitsecs
        self.timer_item = None
        self.kill_request = False
        self.autorestart = autorestart
        self.nouaf = nouaf
        self.quotas = quotas
        self.prv = prv
        self.kt_limit = kt_limit
        self.baspri = baspri
        self.exitcodes = exitcodes

        self.process = ProcessInfo()

    def process_exists(self) -> bool:
        return self.process.pid in self.running_processes

    def process_is_backoff(self) -> bool:
        return self.process.state == ProcessStates.BACKOFF

    def process_is_fatal(self) -> bool:
        return self.process.state == ProcessStates.FATAL

    def process_is_stopped(self) -> bool:
        return self.process.state == ProcessStates.STOPPED

    def process_is_stopping(self) -> bool:
        return self.process.state == ProcessStates.STOPPING

    def set_running(self):
        self.process.state = ProcessStates.RUNNING
        self.remain_startretries = self.startretries + 1

    # def create_process(self, check_finalsts: bool = True):
    def create_process(self, from_terminated: bool):
        global supervisord_table_name

        if self.kill_request and from_terminated:
            return

        if from_terminated:
            # program has already been started
            if not self.autorestart and self.process.finalsts is not None:
                return

            if (
                self.autorestart is RestartWhenExitUnexpected
                and self.process.finalsts in self.exitcodes
            ):
                return

        if self.process_exists():
            return

        try:
            v = lib.get_logical(
                self.process_name + b'_PID',
                supervisord_table_name,
            )[1]
            pid = PidType(int(v, 16))
            while True:
                try:
                    itime, stime = lib.getjpi(jpidef.JPI__LOGINTIM, pid)[2:]
                    break
                except OSError as e:
                    if e.errno != ssdef.SS__SUSPENDED:
                        raise
            itime = crtl.fix_time(itime)
            assert itime is not None
            self.process.pid = PidType(pid)
            self.process.start_time = datetime.datetime.fromtimestamp(itime)
            self.set_running()

            Program.running_processes[pid] = self
            return
        except OSError as e:
            if e.errno not in (ssdef.SS__NOLOGNAM, ssdef.SS__NONEXPR):
                raise

        if (
            self.process.state == ProcessStates.BACKOFF
            and self.timer_item
            and not self.timer_item.done
        ):
            return

        self.kill_request = False

        if self.startretries > 0:
            if self.remain_startretries == 0:
                self.process.state = ProcessStates.FATAL
                return
            self.remain_startretries -= 1

        if self.timer_item:
            self.timer_item.cancel = True
            self.timer_item = None

        self.process.finalsts = None
        # If the image argument specifies the SYS$SYSTEM:LOGINOUT.EXE,
        #  the UIC of the created process will be the UIC of the caller of $CREPRC,
        # and the UIC parameter is ignored.
        # So, we need to use persona
        userpro = starlet.create_user_profile(usrnam=self.user)[1]
        persona_id = starlet.persona_create(usrpro=userpro)[1]
        persona_previous_id = starlet.persona_assume(persona_id)[1]
        # Create a detach process (stsflg=prvdef.PRV_M_IMPERSONATE)
        # From John Gillings:
        # (https://community.hpe.com/t5/operating-system-openvms/run-uic-vs-sys-creprc/td-p/5171510)
        #         General comment about PRC$M_NOUAF ...
        # When you run LOGINOUT in a detached process *WITHOUT* specifying PRC$M_NOUAF
        # for $CREPRC, or *WITH* /AUTHORIZE on the DCL RUN command, (note that the
        # default is reversed between the two), many of the process attributes you have
        # specified with qualifiers or parameters may be overridden in the resulting
        # process.
        # That includes UIC, process name, quotas and privileges. Behind the scenes,
        # the process is created with whatever you specified, but LOGINOUT replaces
        # them with values from the UAF.
        #
        # From https://docs.vmssoftware.com/vsi-openvms-programming-concepts-manual-volume-i/
        # The SYS$CREPRC system service also does not provide default equivalence names
        # for the logical names SYS$LOGIN, SYS$LOGIN_DEVICE, and SYS$SCRATCH.
        # These logical names are available to the created process only when the
        # specified image is LOGINOUT, and when the PRC$M_NOUAF flag is not set.
        error = None
        try:
            stsflg = (
                prcdef.PRC_M_IMPERSONATE
                | prcdef.PRC_M_PARSE_EXTENDED
                | prcdef.PRC_M_KT_LIMIT
            )
            if self.nouaf:
                stsflg |= prcdef.PRC_M_NOUAF

            pid = PidType(
                starlet.creprc(
                    image=self.image,
                    input=self.command,
                    output=self.stdout_file,
                    error=self.stderr_file,
                    prcnam=self.process_name,
                    mbxunt=Program.mbxunt,
                    quota=self.quotas,
                    prv=self.prv,
                    kt_limit=self.kt_limit,
                    baspri=self.baspri,
                    # prcdef.PRC_M_DETACH and prcdef.PRC_M_IMPERSONATE are synonyms
                    stsflg=stsflg,
                )[1]
            )
        except OSError as e:
            error = e
            self.process.state = ProcessStates.FATAL
            return
        finally:
            starlet.persona_assume(persona_previous_id)
            starlet.persona_delete(persona_id)
            if error is not None:
                logger.warning(
                    f"Can' create process {self.process_name}, error {error}"
                )
        lib.set_logical(
            self.process_name + b'_PID',
            hex(pid)[2:].upper(),
            supervisord_table_name,
        )
        lib.set_logical(
            self.process_name + b'_RUN',
            b'1',
            supervisord_table_name,
        )

        self.process.pid = PidType(pid)
        self.process.start_time = datetime.datetime.now()
        if self.startsecs == 0:
            self.set_running()
        else:
            self.process.state = ProcessStates.STARTING
            self.timer_item = TimerItem(TimerItemType.PROC_STARTING, self)
            timer_queue.put(
                PrioritizedItem(current_tick + self.startsecs, self.timer_item)
            )
        Program.running_processes[PidType(pid)] = self
        lib.set_logical(
            self.process_name + b'_BEG',
            str(self.process.start_time)[:19],
            supervisord_table_name,
        )
        try:
            lib.delete_logical(
                self.process_name + b'_END',
                supervisord_table_name,
            )
        except OSError:
            pass

        logging.info(
            f'Process {self.process_name.decode()} created {hex(pid)[2:].upper()}'
        )

    def kill(self):
        if self.process_is_stopped():
            return

        if self.process_is_backoff() or self.process_is_fatal():
            self.process.state = ProcessStates.STOPPED
            self.remain_startretries = self.startretries + 1
            if self.timer_item:
                self.timer_item.cancel = True
            self.timer_item = None
            return

        if not self.process_exists() or self.kill_request:
            return

        self.kill_request = True

        pid = self.process.pid
        if self.process.state == ProcessStates.STOPPING:
            starlet.delprc(self.process.pid)
        else:
            starlet.forcex(pid, code=ssdef.SS__FORCEX)
        self.process.state = ProcessStates.STOPPING
        if self.timer_item:
            self.timer_item.cancel
        self.timer_item = TimerItem(TimerItemType.PROC_STOPPING, self)
        timer_queue.put(
            PrioritizedItem(current_tick + self.stopwaitsecs, self.timer_item)
        )

    def set_terminated(self, finalsts: int, end_time: datetime.datetime):
        global supervisord_table_name
        assert self.process is not None
        pid = self.process.pid
        finalsts = finalsts & 0xEFFFFFFF
        self.process.finalsts = finalsts
        self.process.end_time = end_time
        del Program.running_processes[pid]
        if self.timer_item:
            self.timer_item.cancel = True
            self.timer_item = None
        lib.set_logical(
            self.process_name + b'_RUN',
            b'0',
            supervisord_table_name,
        )
        lib.set_logical(
            self.process_name + b'_END',
            str(end_time)[:22],
            supervisord_table_name,
        )
        if self.process.state == ProcessStates.STARTING:
            self.process.state = ProcessStates.BACKOFF
            self.timer_item = TimerItem(TimerItemType.PROC_BACKOFF, self)
            timer_queue.put(
                PrioritizedItem(
                    5
                    + current_tick
                    + self.startretries
                    - self.remain_startretries
                    + 1,
                    self.timer_item,
                )
            )
        else:
            self.process.state = (
                ProcessStates.STOPPED
                if self.kill_request
                else ProcessStates.EXITED
            )


def logicals_init():
    global supervisord_table_name
    starlet.crelnt(
        attr=lnmdef.LNM_M_CREATE_IF,
        promsk=0xE000,
        tabnam=supervisord_table_name,
        partab=b'LNM$SYSTEM_DIRECTORY',
        acmode=psldef.PSL_C_SUPER,
    )
    lib.set_logical(
        b'LNM$PERMANENT_MAILBOX',
        supervisord_table_name,
        b'LNM$PROCESS_DIRECTORY',
    )


def mbx_init() -> Tuple[int, int, int]:
    global logger
    logger.info('Creating mailbox')
    s, chan = starlet.crembx(
        prmflg=1,
        lognam='SUPERVISORD_MBX',
        maxmsg=accdef.ACC_K_TERMLEN,
        bufquo=1024 * 64,
        promsk=0x0000FF00,
        acmode=psldef.PSL_C_USER,
        flags=cmbdef.CMB_M_READONLY,
    )
    r = lib.getdvi(dvidef.DVI__UNIT, chan)
    mbxunt = r[1]

    s, chancmd = starlet.crembx(
        prmflg=1,
        lognam='SUPERVISORD_CMD',
        maxmsg=2048,
        bufquo=8192,
        promsk=0x0000FF00,
        acmode=psldef.PSL_C_USER,
        flags=cmbdef.CMB_M_READONLY,
    )
    logger.info(f'Mailbox created {mbxunt}')
    return chan, mbxunt, chancmd   # , chancmd_r


class AstParam(object):
    ptype: AstParamType
    iosb: iosbdef.IOSB_r_io_64 | None
    buff: ctypes.Array[ctypes.c_char] | None

    def __init__(
        self,
        ptype: AstParamType,
        iosb: iosbdef.IOSB_r_io_64 | None = None,
        buff: ctypes.Array[ctypes.c_char] | None = None,
    ):
        self.ptype = ptype
        self.iosb = iosb
        self.buff = buff


def qio_procterm(fterm: mbxqio.MBXQIO) -> vmsast.AstContext:
    astctxt = vmsast.AstContext(flags=vmsast.M_QUEUE | vmsast.M_WAKE)
    iosb, tbuff = fterm.read_nowait(accdef.ACC_K_TERMLEN, astctxt)
    astctxt.param = AstParam(AstParamType.PROCTERM, iosb, tbuff)
    return astctxt


def qio_cmd(fcmd: mbxqio.MBXQIO) -> vmsast.AstContext:
    astctxt = vmsast.AstContext(flags=vmsast.M_QUEUE | vmsast.M_WAKE)
    iosb, cbuff = fcmd.read_nowait(256, astctxt)
    astctxt.param = AstParam(AstParamType.CMD, iosb, cbuff)
    return astctxt


def send_cmd_reply(mbxreply_name: str, cmdreply: dict):
    with mbxqio.MBXQIO(mbxname=mbxreply_name) as fcmd_r:
        jscmd = json.dumps(cmdreply)
        try:
            fcmd_r.write(
                jscmd.encode('ascii'),
                iodef.IO_M_READERCHECK | iodef.IO_M_NOW,
            )
        except OSError:
            pass


def dispatch_cmd(res: bytes):
    global supervisorctl_pwd
    jscmd = json.loads(res)
    pwd = jscmd['pwd']
    jscmd['pwd'] = '*****'
    fcmd_r = jscmd['mbxreply']
    if pwd != supervisorctl_pwd:
        logging.info('Invalid password')
        send_cmd_reply(fcmd_r, {'error': 'Invalid password'})

        return
    match jscmd['cmd'].lower():
        case 'shutdown':
            send_cmd_reply(fcmd_r, {'result': None})
            exit(1)
        case 'start':
            pgmnames = set([name.upper() for name in jscmd['programs']])
            if 'ALL' in pgmnames:
                for pgm in Program.programs.values():
                    if pgm.process.state == ProcessStates.FATAL:
                        pgm.process.state = ProcessStates.STOPPED
                        pgm.remain_startretries = pgm.startretries + 1
                    pgm.create_process(False)
            else:
                for pgm in Program.programs.values():
                    if pgm.name in pgmnames:
                        if pgm.process.state == ProcessStates.FATAL:
                            pgm.process.state = ProcessStates.STOPPED
                            pgm.remain_startretries = pgm.startretries + 1
                        pgm.create_process(False)
            send_cmd_reply(fcmd_r, {'result': None})
        case 'stop':
            pgmnames = set([name.upper() for name in jscmd['programs']])
            if 'ALL' in pgmnames:
                for pgm in Program.programs.values():
                    pgm.kill()
            else:
                for pgm in Program.programs.values():
                    if pgm.name in pgmnames:
                        pgm.kill()
            send_cmd_reply(fcmd_r, {'result': None})
        case 'status':
            pgmnames = set([name.upper() for name in jscmd['programs']])
            lst = []
            for pgm in Program.programs.values():
                if pgmnames and pgm.name not in pgmnames:
                    continue
                lst.append(
                    [
                        pgm.process_name.decode('ascii'),
                        hex(pgm.process.pid)[2:].upper(),
                        str(pgm.process.start_time)[:19],
                        pgm.process.state,
                    ]
                )
                if pgm.process.state in (
                    ProcessStates.BACKOFF,
                    ProcessStates.STOPPED,
                    ProcessStates.EXITED,
                    ProcessStates.FATAL,
                ):
                    lst[-1].append(str(pgm.process.end_time)[:19])
                    lst[-1].append(pgm.process.finalsts)

            send_cmd_reply(fcmd_r, {'result': lst})


def dispatch_ast(astparam: AstParam):
    iosb: iosbdef.IOSB_r_io_64 | None

    iosb = astparam.iosb
    buff = astparam.buff
    match astparam.ptype:
        case AstParamType.PROCTERM:
            assert iosb is not None and buff is not None
            res = buff.raw[: iosb.iosb_r_devdepend.iosb_r_bcnt_16.iosb_w_bcnt]
            pid = iosb.iosb_r_devdepend.iosb_r_bcnt_16.iosb_l_dev_depend
            try:
                pgm = Program.running_processes[pid]
                acc = accdef.ACCDEF.parse(res)
                print(acc)
                logging.info(
                    f'Program {pgm.name} '
                    f'terminated {hex(pid)[2:].upper()}, '
                    f'{acc.acc_l_finalsts}'
                )
                pgm.set_terminated(acc.acc_l_finalsts, acc.acc_q_termtime)
                # if not stsdef.vms_status_success(acc.acc_l_finalsts):
                pgm.create_process(True)
            except KeyError:
                pass
        case AstParamType.CMD:
            assert iosb is not None and buff is not None
            res = buff.raw[: iosb.iosb_r_devdepend.iosb_r_bcnt_16.iosb_w_bcnt]
            pid = iosb.iosb_r_devdepend.iosb_r_bcnt_16.iosb_l_dev_depend
            dispatch_cmd(res)
        case AstParamType.TIMER:
            global current_tick
            current_tick += 1
            while True:
                itm: PrioritizedItem
                try:
                    itm = timer_queue.get_nowait()
                except queue.Empty:
                    return
                if itm.item.cancel:
                    continue
                if itm.priority > current_tick:
                    timer_queue.put(itm)
                    return
                itm.item.done = True
                val: Program = itm.item.value
                match itm.item.typ:
                    case TimerItemType.PROC_STARTING:
                        val.set_running()
                    case TimerItemType.PROC_BACKOFF:
                        val.create_process(False)
                    case TimerItemType.PROC_STOPPING:
                        val.kill()


def run(chan: int, chancmd: int):
    global timer_delay, timer_astctx
    astparam: AstParam
    with (
        mbxqio.MBXQIO(channel=chan) as fterm,
        mbxqio.MBXQIO(channel=chancmd) as fcmd,
    ):
        t_astctxt = qio_procterm(fterm)  # noqa: F841
        c_astctxt = qio_cmd(fcmd)  # noqa: F841

        for pgm in sorted(
            Program.programs.values(), key=lambda pgm: pgm.priority
        ):
            if pgm.autostart:
                pgm.create_process(False)

        timer_astctx.param = AstParam(AstParamType.TIMER)
        starlet.setimr(daytim=timer_delay, reqidt=timer_astctx)

        timer_in_progress = True
        while True:
            ret_ast_context: vmsast.AstContext

            ret_ast_context = vmsast.get_completed(True)   # type:ignore
            astparam = ret_ast_context.param   # type: ignore
            iosb = astparam.iosb
            if iosb and iosb.iosb_w_status == ssdef.SS__ENDOFFILE:
                if astparam.ptype == AstParamType.PROCTERM:
                    t_astctxt = qio_procterm(fterm)
                elif astparam.ptype == AstParamType.CMD:
                    c_astctxt = qio_cmd(fcmd)
                continue
            if iosb and not stsdef.vms_status_success(iosb.iosb_w_status):
                raise IOError(iosb.iosb_w_status)
            dispatch_ast(astparam)
            if astparam.ptype == AstParamType.PROCTERM:
                t_astctxt = qio_procterm(fterm)  # noqa: F841
            elif astparam.ptype == AstParamType.CMD:
                c_astctxt = qio_cmd(fcmd)  # noqa: F841
            elif astparam.ptype == AstParamType.TIMER:
                timer_in_progress = False

            if timer_queue.not_empty and not timer_in_progress:
                timer_in_progress = True
                start_timer()


def is_deamon() -> bool:
    try:
        pid = lib.get_logical(
            b'SUPERVISORD' + b'_PID',
            supervisord_table_name,
        )[1]
        cpid = lib.getjpi(jpidef.JPI__PID)[3]
        return pid == cpid
    except OSError as e:
        if e.errno != ssdef.SS__NOLOGNAM:
            raise
        return False


def daemon_is_running() -> bool:
    try:
        v = lib.get_logical(
            b'SUPERVISORD' + b'_PID',
            supervisord_table_name,
        )[1]
        pid = int(v, 16)
        lib.getjpi(jpidef.JPI__PID, pid)
        return True
    except OSError as e:
        if e.errno == ssdef.SS__SUSPENDED:
            return True
        if e.errno == ssdef.SS__NONEXPR:
            return False
        raise


def daemonize(usrdaemon: str, comdaemon: str, stdout_file: str) -> int | None:
    userpro = starlet.create_user_profile(usrnam=usrdaemon)[1]
    persona_id = starlet.persona_create(usrpro=userpro)[1]
    persona_previous_id = starlet.persona_assume(persona_id)[1]
    error = None
    try:
        stsflg = (
            prcdef.PRC_M_IMPERSONATE
            | prcdef.PRC_M_PARSE_EXTENDED
            | prcdef.PRC_M_KT_LIMIT
            | prcdef.PRC_M_HIBER
        )

        pid = PidType(
            starlet.creprc(
                image=b'SYS$SYSTEM:LOGINOUT.EXE',
                input=comdaemon.encode(),
                output=stdout_file.encode(),
                prcnam=b'SUPERVISORD',
                kt_limit=1,
                baspri=4,
                stsflg=stsflg,
            )[1]
        )
        lib.set_logical(
            b'SUPERVISORD' + b'_PID',
            hex(pid)[2:].upper(),
            supervisord_table_name,
        )
        starlet.wake(pid)
        return pid
    except OSError as e:
        error = e
        return None
    finally:
        starlet.persona_assume(persona_previous_id)
        starlet.persona_delete(persona_id)
        if error is not None:
            print(f"Can' create process SUPERVISORD, error {error}")


def main():
    global logger, supervisorctl_pwd

    # Required privileges: ALTPRI, SYSNAM, PRMMBX, IMPERSONATE, SETPRV, WORLD
    starlet.setprv(
        1,
        prvdef.PRV_M_SYSNAM
        | prvdef.PRV_M_ALTPRI
        | prvdef.PRV_M_PRMMBX
        | prvdef.PRV_M_SETPRV
        | prvdef.PRV_M_IMPERSONATE
        | prvdef.PRV_M_WORLD,
    )

    parser = argparse.ArgumentParser(description='supervisord')
    parser.add_argument(
        '-c',
        '--configuration',
        type=argparse.FileType('r', encoding='latin1'),
        default='./supervisord.conf',
        help='Configuration file path (default ./supervisord.conf)',
    )
    parser.add_argument(
        '-p',
        '--password',
        required=False,
        type=str,
        default='',
        help='Password for supervisord',
    )
    parser.add_argument(
        '-n',
        '--nodaemon',
        action='store_true',
        help='Run supervisord in the foreground',
    )
    args = parser.parse_args()
    supervisorctl_pwd = args.password

    config = configparser.ConfigParser()
    config.read_file(args.configuration)
    args.configuration.close()

    nodaemon = config['supervisord'].getboolean('nodaemon', False) or args.nodaemon
    if not nodaemon and not is_deamon():
        usrdaemon = config['supervisord'].get('user', 'system')
        stdout_file = config['supervisord'].get('stdout_file', 'NLA0:')
        comdaemon = config['supervisord']['command']
        pid = daemonize(usrdaemon, comdaemon, stdout_file)
        if pid is None:
            print('Unable to start supervisord daemon')
        else:
            print(f'Daemon started {hex(pid)[2:].upper()}')
        exit(0)

    logfile = config['supervisord'].get('logfile', 'supervisord.log')

    logging_format = '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
    logging.basicConfig(
        filename='/dev/null', level=logging.INFO, format=logging_format
    )
    formatter = logging.Formatter(logging_format)
    main_log = logging.getLogger()  # root handler
    main_log.setLevel(logging.INFO)
    logfn = crtl.from_vms(logfile)
    if logfn is None:
        print(f'{repr(logfile)} is invalid')
        crtl.vms_exit(ssdef.SS__INVARG)
    handler = logging.handlers.TimedRotatingFileHandler(
        logfn, 'midnight', backupCount=10
    )
    handler.setFormatter(formatter)
    main_log.addHandler(handler)

    for sn in config.sections():
        if sn.startswith('program:'):
            quotaslst = [
                quota.strip().upper().split('=')
                for quota in config[sn].get('quotas', '').split(',')
                if quota != ''
            ]
            prcquotas = []
            for quotan, quotav in quotaslst:
                if quotan not in quotas:
                    print(f'{sn}: {quotan}={quotav} not a valid quota')
                    crtl.vms_exit(ssdef.SS__INVARG)
                if quotan == 'CPULM':
                    quotav = int(quotav) * 100
                prcquotas.append((quotas[quotan], int(quotav)))
            if prcquotas == []:
                prcquotas = None

            prvnames = [
                name.strip().upper()
                for name in config[sn].get('privileges', '').split(',')
                if name != ''
            ]
            prv: PrvMask | None
            prv = None
            for prvnam in prvnames:
                if prvnam not in privileges:
                    print(f'{sn}: {prvnam} not a valid privilege')
                    crtl.vms_exit(ssdef.SS__INVARG)
                prv = (
                    privileges[prvnam]
                    if prv is None
                    else PrvMask(prv | privileges[prvnam])
                )

            name = sn.split(':')[-1].upper()
            process_name = config[sn].get('process_name', name)
            autostart = config[sn].getboolean('autostart', True)
            command = config[sn].get('command', 'NLA0:')
            image = config[sn].get('image', 'SYS$SYSTEM:LOGINOUT.EXE')
            stdout_file = config[sn].get('stdout_file', 'NLA0:')
            stderr_file = config[sn].get('stderr_file', 'NLA0:')
            priority = config[sn].getint('priority', 999)
            baspri = config[sn].getint('baspri', 4)
            nouaf = config[sn].getboolean('nouaf', False)
            kt_limit = config[sn].getint('kt_limit', 0)
            startsecs = config[sn].getint('startsecs', 10)
            startretries = config[sn].getint('startretries', 3)
            stopwaitsecs = config[sn].getint('stopwaitsescs', 10)
            autorestart = auto_restart(
                config[sn].get('autorestart', 'unexpected')
            )
            exitcodes = config[sn].get('exitcodes', '1').split(',')
            user = config[sn]['user']
            if prv is not None or quotaslst is not None:
                nouaf = True
            p = Program(
                name=name,
                user=user,
                command=command,
                image=image,
                process_name=process_name,
                autostart=autostart,
                priority=priority,
                stdout_file=stdout_file,
                stderr_file=stderr_file,
                startsecs=startsecs,
                startretries=startretries,
                stopwaitsecs=stopwaitsecs,
                autorestart=autorestart,
                nouaf=nouaf,
                quotas=prcquotas,
                prv=prv,
                baspri=baspri,
                kt_limit=kt_limit,
                exitcodes=[
                    int(exitcode) for exitcode in exitcodes if exitcode != ''
                ],
            )
            Program.programs[name] = p

    logicals_init()
    chan, Program.mbxunt, chancmd = mbx_init()
    run(chan, chancmd)


if __name__ == '__main__':
    main()