#!/usr/bin/env python3

# SPDX-FileCopyrightText: 2009 Fermi Research Alliance, LLC
# SPDX-License-Identifier: Apache-2.0

# Description:
#  This program updates a glidein factory directory structure
#  based on a configuration file


import logging
import os
import os.path
import shutil
import signal
import sys
import tarfile
import tempfile

# from glideinwms.creation.lib import cWConsts
# from glideinwms.creation.lib import cgWParams
# from glideinwms.creation.lib import cgWDictFile
from glideinwms.creation.lib import cgWConsts, cgWCreate, cgWParamDict, factoryXmlConfig, xslt
from glideinwms.factory import glideFactoryConfig, glideFactoryLib, glideFactoryMonitorAggregator
from glideinwms.lib import logSupport
from glideinwms.lib.util import handle_hooks, HOOK_POST_RECONFIG_DIRNAME, HOOK_PRE_RECONFIG_DIRNAME

STARTUP_DIR = "/var/lib/gwms-factory/web-base"
sys.path.append(os.path.join(STARTUP_DIR, "../.."))

FACTORY_DIR = os.path.dirname(glideFactoryLib.__file__)

# necessary because systemd is not on rhel6
try:
    from systemd import journal

    def print2(message):
        print(message)
        journal.send(str(message))

except ImportError:

    def print2(message):
        print(message)


force_delete = False
fix_rrd = False


class UsageError(Exception):
    pass


class ReconfigError(Exception):
    pass


def logReconfig(msg):
    glideinDescript = glideFactoryConfig.GlideinDescript()
    # Set the Log directory
    logSupport.log_dir = os.path.join(glideinDescript.data["LogDir"], "factory")
    # Configure factory process logging
    process_logs = eval(glideinDescript.data["ProcessLogs"])
    for plog in process_logs:
        if "ADMIN" in plog["msg_types"]:
            logSupport.add_processlog_handler(
                "factoryadmin",
                logSupport.log_dir,
                "DEBUG,INFO,WARN,ERR",
                plog["extension"],
                int(float(plog["max_days"])),
                int(float(plog["min_days"])),
                int(float(plog["max_mbytes"])),
                plog["compression"],
            )
        else:
            logSupport.add_processlog_handler(
                "factoryadmin",
                logSupport.log_dir,
                plog["msg_types"],
                plog["extension"],
                int(float(plog["max_days"])),
                int(float(plog["min_days"])),
                int(float(plog["max_mbytes"])),
                plog["compression"],
            )
    logSupport.log = logging.getLogger("factoryadmin")
    logSupport.log.info("Reconfiguring factory: %s" % msg)


def main(conf, update_scripts, update_def_cfg, comment=""):
    old_glidein_dicts_obj = None
    # create dictionaries for new params
    glidein_dicts_obj = cgWParamDict.glideinDicts(conf)

    # load old files
    if os.path.exists(glidein_dicts_obj.main_dicts["glidein"].get_filepath()):
        old_glidein_dicts_obj = cgWParamDict.glideinDicts(conf)
        old_glidein_dicts_obj.load()

    try:
        glidein_dicts_obj.populate(old_glidein_dicts_obj)
    except cgWParamDict.UnconfiguredScheddError as e:
        print2(e.err_str)
        sys.exit(1)

    # merge them together
    if old_glidein_dicts_obj is not None:
        entries = set(glidein_dicts_obj.main_dicts["glidein"]["Entries"].split(","))
        old_entries = old_glidein_dicts_obj.main_dicts["glidein"]["Entries"].split(",")
        disabled_entries = glidein_dicts_obj.main_dicts.disabled_sub_list
        for entry in old_entries:
            if (entry) and (entry not in entries) and (entry not in disabled_entries):
                print2("WARNING: entry %s is not found in new xml!" % entry)
                if not force_delete:
                    print2("Aborting reconfig since this is just too scary.")
                    print2("Enable -force_delete if you really want to delete it.")
                    sys.exit(1)

        glidein_dicts_obj.reuse(old_glidein_dicts_obj)
    else:
        # If no old params exist, make sure to create the new directory, (no reuse)
        glidein_dicts_obj.create_dirs(fail_if_exists=False)

    # write to disk
    glidein_dicts_obj.save()
    glidein_dicts_obj.set_readonly(True)

    if update_scripts == "yes":
        # copy the submit files
        cgWCreate.copy_exe(cgWConsts.STARTUP_FILE, glidein_dicts_obj.main_dicts.work_dir, cgWConsts.WEB_BASE_DIR, True)
        cgWCreate.copy_exe(
            cgWConsts.LOCAL_START_WRAPPER, glidein_dicts_obj.main_dicts.work_dir, cgWConsts.WEB_BASE_DIR, True
        )

        # augment glidein_startup.sh, appending the utility files/data to glidein_startup_base.sh as tarball:
        # create tarball of files to be later sourced and shared between scripts
        tar_file_list = cgWConsts.STARTUP_FILE_PAYLOAD
        try:
            with tarfile.open("tar_utils.tar.gz", "w:gz", dereference=True) as tar_utils:
                for util_file in tar_file_list:
                    util_file_path = os.path.join(cgWConsts.WEB_BASE_DIR, util_file)
                    tar_utils.add(util_file_path, arcname=os.path.basename(util_file_path))
        except tarfile.TarError as te:
            print2("Unable to pack the startup file payload. TarError: %s" % str(te))
            sys.exit(1)
        # append the the tarball to glidein_startup.sh
        try:
            with open(os.path.join(glidein_dicts_obj.main_dicts.work_dir, cgWConsts.STARTUP_FILE), "ab") as st_file:
                st_file.write(b"exit 0\n#EOF\n")
                with open("tar_utils.tar.gz", "rb") as tar_utils:
                    shutil.copyfileobj(tar_utils, st_file)
        except OSError as ioe:
            print2("Unable to append the payload to the startup file. IOError [%d]: %s" % (ioe.errno, ioe.strerror))
            sys.exit(1)
        os.remove("tar_utils.tar.gz")
        print2("...Updated the glidein_startup.sh and local_start.sh scripts")

        # copy glidein_startup.sh from work dir (the version w/ the utility files) to the stage area for cloud entries
        cgWCreate.copy_exe(
            cgWConsts.STARTUP_FILE, glidein_dicts_obj.main_dicts.stage_dir, glidein_dicts_obj.main_dicts.work_dir, True
        )
        print2("...Updated the glidein_startup.sh file in the staging area")

        # copy helper executables
        cgWCreate.copy_exe(
            cgWConsts.UPDATE_PROXY_FILE, glidein_dicts_obj.main_dicts.work_dir, cgWConsts.WEB_BASE_DIR, True
        )

    if update_def_cfg == "yes" or update_scripts == "yes":
        # recreate the init.d startup file
        # This will never happen in RPM installations (because of the init.d file)
        startup_fname = os.path.join(glidein_dicts_obj.main_dicts.work_dir, cgWConsts.INITD_STARTUP_FILE)
        factory_dir = glidein_dicts_obj.main_dicts.work_dir

        # Remove startup file if already exists
        if os.path.exists(os.path.join(factory_dir, startup_fname)):
            os.remove(os.path.join(factory_dir, startup_fname))

        cgWCreate.create_initd_startup(
            startup_fname, factory_dir, os.path.realpath(os.path.join(STARTUP_DIR, "..")), conf.file
        )
        print2("...Updated the factory_startup script")

        if update_def_cfg == "yes":
            print2("...Updated default config file location to: %s" % conf.file)

    print2("...Reconfigured glidein '%s' is complete" % conf["glidein_name"])
    print2("...Active entries are:")
    for entry in glidein_dicts_obj.active_sub_list:
        print2("     %s" % entry)
    print2("...Verifying rrd schema")
    if not glideFactoryMonitorAggregator.verifyRRD(fix_rrd):
        if not fix_rrd:
            print2("Run with -fix_rrd option to update errors")
            print2("WARNING: back up your existing rrds before auto-fixing rrds")
        sys.exit(1)
    print2("...Submit files are in %s" % glidein_dicts_obj.main_dicts.work_dir)
    if comment:
        logReconfig(
            "Reconfig successful with options update_def_cfg='%s' update_scripts='%s' comment='%s'"
            % (update_def_cfg, update_scripts, comment)
        )


############################################################
#
# S T A R T U P
#
############################################################

if __name__ == "__main__":
    exit_code = 0
    usage = "usage: reconfig_glidein { -force_name name -writeback yes|no -update_scripts yes|no -xml xml -update_def_cfg yes|no [-force_delete] [-xslt_plugin_dir xdir] | -help }"
    argv = sys.argv

    if len(argv) == 1:
        print2(usage)
        sys.exit(1)

    if os.geteuid() == 0:
        print(
            "NOTE: Executing reconfig_glidein as user 'root' is not allowed. Use the factory user instead. For rpm based installations, use the 'service gwms-factory <start|stop|reconfig|...>' command to perform gwms-factory operations"
        )

    force_name = None
    writeback = "no"
    update_scripts = "no"
    sighupreload = False
    xml = ""
    comment = ""
    update_def_cfg = "no"

    xslt_plugin_dir = os.environ.get("GWMS_XSLT_PLUGIN_DIR", None)

    for i in range(len(argv)):
        if argv[i] == "-fix_rrd":
            fix_rrd = True
        if argv[i] == "-sighupreload":
            sighupreload = True
        if argv[i] == "-comment":
            comment = "(" + argv[i + 1] + ")"
        if argv[i] == "-force_name":
            force_name = argv[i + 1]
        if argv[i] == "-writeback":
            writeback = argv[i + 1]
        elif argv[i] == "-force_delete":
            force_delete = True
        if argv[i] == "-update_scripts":
            update_scripts = argv[i + 1]
        if argv[i] == "-xml":
            xml = argv[i + 1]
        if argv[i] == "-xslt_plugin_dir":
            xslt_plugin_dir = argv[i + 1]
        if argv[i] == "-update_def_cfg":
            update_def_cfg = argv[i + 1]
        if argv[i] == "-help":
            print2(usage)
            sys.exit(1)

    handle_hooks(os.path.dirname(os.path.abspath(xml)), HOOK_PRE_RECONFIG_DIRNAME)

    if sighupreload:
        signal.signal(signal.SIGHUP, signal.SIG_IGN)
    # conf is of type factoryXmlConfig.Config
    # pylint: disable=maybe-no-member
    conf = factoryXmlConfig.parse(xml)

    try:
        transformed_xmlfile = tempfile.NamedTemporaryFile()
        transformed_xmlfile.write(xslt.xslt_xml(old_xmlfile=xml, xslt_plugin_dir=xslt_plugin_dir))
        transformed_xmlfile.flush()

        args = [argv[0], transformed_xmlfile.name]

        glidein_name = conf["glidein_name"]
        if force_name is not None:
            if glidein_name != force_name:
                raise UsageError(f"This is not a '{force_name}' config file ('{glidein_name}' found)")

        if not (writeback in ("yes", "no")):
            raise UsageError("-writeback must be yes or no, found '%s'" % writeback)

        if not (update_def_cfg in ("yes", "no")):
            raise UsageError("-update_def_cfg must be yes or no, found '%s'" % update_def_cfg)

        try:
            # This is the current running version, saved in the glidein work dir
            submit_dir = conf.get_submit_dir()

            main(conf, update_scripts, update_def_cfg, comment=comment)

        except RuntimeError as e:
            raise ReconfigError(str(e))

    except ReconfigError as re:
        print2(re)
        exit_code = 1

    except UsageError as ue:
        print2(usage)
        print("")
        print2(ue)
        exit_code = 1

    except RuntimeError as e:
        print2(e)
        exit_code = 1

    handle_hooks(os.path.dirname(os.path.abspath(xml)), HOOK_POST_RECONFIG_DIRNAME)

    if sighupreload:
        print2("switching back to the main glideFactory process")
        os.execv(os.path.join(FACTORY_DIR, "glideFactory.py"), ["glideFactory.py", "/var/lib/gwms-factory/work-dir"])

    sys.exit(exit_code)
