#!/usr/bin/env python3
# Copyright 2026 SigmaSRC, Inc. All Rights Reserved.

"""Run a fuel script.

Usage: runfuel.py [-f] [-g] [-s server] [-t ostype] [-e entrypoint] file [arg ...]

The file must have a .fuel extension, and it must define a Fuel proc
with the following signature:

    proc main(host: Host):
        ...

If an -e argument is given, it is used instead of main.

If any arguments are given, they define the names, types and values of
additional arguments for the entry point, of the form
'<name>:<type>=<value>', where <name> is the argument name, <type>
must be 'int', 'float', 'bool' or 'str', and <value> must match the
Fuel syntax for <type>, except if <type> is 'str', <value> is the rest
of the argument; no string quotes are required or allowed.

"""

# Python imports
import os
import re
import sys
import argparse

# Fuel imports
from sigma.util import avalog
from sigma.util.errors import usage, error
from sigma.util import ostypes, paramtypes
from sigma.fuelc import modreg, typesys
from sigma.fuelc.compile import CompilationManager

parser = argparse.ArgumentParser(description=__doc__.strip(),
                                 formatter_class=argparse.RawDescriptionHelpFormatter)
parser.add_argument("-e", "--entrypoint", dest="entrypoint", default="main",
                    help="alternate entry point (see above)")
parser.add_argument("-f", "--enforce", dest="enforce", action="store_true",
                    help="enable all enforcement actions (default off)")
parser.add_argument("-g", "--generate", dest="runit", action="store_false",
                    default=True,
                    help="write start script on stdout instead of running it")
parser.add_argument("-s", "--server", dest="hostname",
                    help="run the script against the server database for HOSTNAME")
parser.add_argument("-t", "--ostype", dest="ostype",
                    help="append _OSTYPE.fuel to file (sort of)")
parser.add_argument("-T", "--template-id", dest="tmplid", action="store",
                    help="set the template ID", type=int)
parser.add_argument("-R", "--rule-id", dest="ruleid", action="store",
                    help="set the rule ID", type=int)
parser.add_argument("-A", "--allow", dest="pwmode", action="store_const",
                    const="allow", help="set poliwall global allow mode")
parser.add_argument("-D", "--deny", dest="pwmode", action="store_const",
                    const="deny", help="set poliwall global deny mode")
parser.add_argument("-M", "--directive-id", dest="directiveid", action="store",
                    help="set the directive ID", type=int)
parser.add_argument("-V", "--directive-ver", dest="directivever", action="store",
                    help="set the directive version", type=int)
parser.add_argument("args", nargs="*", help="fuel filename and optional arguments")

def main(args=None):
    avalog.setupLogging()
    opts = parser.parse_args(args)
    args = opts.args
    if opts.hostname and opts.enforce:
        return usage("you can't use -s/--server and -f/--enforce together")
    if not re.match(r"\A[a-zA-Z_]\w*\Z", opts.entrypoint):
        return usage("invalid entry point name:", opts.entrypoint)
    if len(args) < 1:
        return usage("please pass a fuel filename argument")
    cm = CompilationManager()
    filename = args[0]
    if opts.ostype:
        if filename.endswith(".fuel"):
            return usage("-tOSTYPE requires a partial filename "
                         "not ending in .fuel")
        if not ostypes.isvalid(opts.ostype):
            return usage("invalid -tOSTYPE option; value OSTYPE values:",
                         ", ".join(ostypes.validtypes()))
        fn = ostypes.search(cm.fuelpath, filename, opts.ostype)
        if fn is None:
            return error("can't find %s_%s.fuel or substitute along %s",
                         filename, opts.ostype, cm.fuelpath)
        sys.stderr.write("using %s\n" % fn)
        filename = fn
    else:
        if not filename.endswith(".fuel"):
            return usage("please pass a filename of the form *.fuel")
    rv = genfuelstart(cm, filename, opts, args[1:])
    if isinstance(rv, int):
        return rv
    assert isinstance(rv, str)
    if not opts.runit:
        sys.stdout.write(rv)
    else:
        exec(rv, {})
    return None

def genfuelstart(cm, filename, opts, argspecs=[]):
    # XXX The return value is problematic:
    #     if there's an error, this returns an int (proposed exit status);
    #     if all's well, this returns a string (Python program to run).
    #     What to do about that?  Maybe write the Python program to a file?

    # Process the argument specs
    argvalues = []
    argtuples = []
    for a in argspecs:
        m = re.match(r"\A([a-zA-Z_]\w*):(\w+(?:\s\w+)*)=(.*)\Z", a)
        if not m:
            return usage("invalid argument spec:", a)
        aname, atype, avalue = m.group(1, 2, 3)
        try:
            avalue = paramtypes.convert(avalue, atype)
        except paramtypes.ParamError as err:
            return usage("%s: %s", err.__class__.__name__, err)
        argvalues.append(avalue)
        argtuples.append((aname, atype))

    # Compute the module name from the filename
    assert filename.endswith(".fuel"), filename
    filebase = filename[:-5]
    modname = os.path.basename(filebase)

    try:
        # Load the module
        defn = cm.loadmodule(modname, os.path.realpath(filename))
    except modreg.LoadError as err:
        return error(str(err))

    if opts.runit:
        try:
            # Check that the input file defines the entry point correctly
            cm.check_entrypoint(defn.value, opts.entrypoint, argtuples)
        except modreg.LoadError as err:
            return error("%s: %s" % (filename, err))

    # Now produce the text
    lines = []

    # XXX This sys.path editing is problematic, since it uses the
    # current path, which may not be right when the code is shipped to
    # the client.
    lines.append("import sys")
    lines.append("sys.path.extend(%r)" % cm.fuelpath)
    lines.append("sys.path.insert(0, %r)" % os.path.dirname(filename))
    lines.append("from sigma.util.fuelset import FuelSet")

    # XXX I wonder if "-g hostname" should be made an option to the
    # generated script?
    if opts.hostname:
        lines.append("from sigma.serverfuel import dbrtlib as rtlib")
        lines.append("host = rtlib.Host(%r)" % opts.hostname)
    else:
        lines.append("from sigma.runtime import rtlib")
        lines.append("host = rtlib.Host('localhost')")
        if opts.enforce:
            lines.append("host.pwall_enforce = True")
            lines.append("host.enforce = True")
        if opts.pwmode is not None:
            lines.append("host.pwall_mode = %r" % opts.pwmode)
    if opts.tmplid:
        lines.append("host.settmplid(%d)" % opts.tmplid)
    if opts.ruleid:
        lines.append("host.setruleid(%d)" % opts.ruleid)
    if opts.directiveid:
        if opts.directivever:
            lines.append("host.setdirectiveid(%d, %d)" % (opts.directiveid, opts.directivever))
        else:
            lines.append("host.setdirectiveid(%d, None)" % opts.directiveid)

    argstrs = list(map(repr, argvalues))
    argstrs.insert(0, "host")
    lines.append("import %s_fuel" % modname)
    lines.append("try:")
    lines.append("    try:")
    lines.append("        %s_fuel.%s(%s)" % (modname, opts.entrypoint,
                                         ", ".join(argstrs)))
    lines.append("    except rtlib.FuelExit as err:")
    lines.append("        sys.exit(err.status)")
    lines.append("finally:")
    lines.append("    host.close()")

    lines.append("")
    return "\n".join(lines)

if __name__ == "__main__":
    # Special case when running under IDLE
    if (not sys.argv[1:] and
        sys.stdout.__class__.__module__.startswith("idlelib.")):
        main(["test.fuel"])
    else:
        sys.exit(main())
