#!/usr/bin/env python3

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

"""This program updates a Glidein Factory directory structure based on a configuration file."""

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

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 = sys.path[0]
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
    logSupport.log = logSupport.get_logger_with_handlers("factoryadmin", logSupport.log_dir, glideinDescript.data)
    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 terr:
            print2("Unable to pack the startup file payload. TarError: %s" % str(terr))
            sys.exit(1)
        # append 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 'systemctl <start|stop|reload|...> gwms-factory' 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 writeback not in ("yes", "no"):
            raise UsageError("-writeback must be yes or no, found '%s'" % writeback)

        if update_def_cfg not 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, ValueError) as e:
            raise ReconfigError(str(e)) from e

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

    except UsageError as uerr:
        print2(usage)
        print("")
        print2(uerr)
        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)
