#!/usr/bin/env python3
# ***************************************************************************
#  IIIIII NNN  NNN  Copyright (C) 2022 Innovative Networks, Inc.
#    II    NNN  N   All Rights Reserved. Any redistribution or reproduction
#    II    N NN N   of part or all of the content of this program in any form
#    II    N  NNN   without expressed written consent of the copyright holder
#  IIIIII NNN  NNN  is strictly prohibited.  Please contact admins@in-kc.com
#   Be Innovative.  for additional information.
# ***************************************************************************
#  sync_inmonscripts.py
#  Author - Ian Perry <iperry@indigex.com>
#
#  Purpose:  Syncs scripts from https://inmonscripts.indigex.com
#
#  Version History:
#       2024.02.07 - Initial Creation
# ***************************************************************************
try:
    import inmon_utils as inmon
except Exception as e:  # pylint: disable=broad-exception-caught
    import sys

    print("Unable to import inmon_utils: %s", e)
    sys.exit(3)

import argparse
import hashlib
import json
import logging
import os
import pprint
import stat
import time
import shutil

import requests

parser = argparse.ArgumentParser(description="Syncs scripts from inmonscripts website.")
parser.add_argument(
    "-d", "--debug", dest="debug", help="Enable debugging output", action="store_true"
)
parser.add_argument(
    "--dry-run", dest="dry_run", help="Perform a dry run.", action="store_true"
)
parser.add_argument(
    "--satellite", dest="satellite", help="Also pulls sattelite scripts", action="store_true"
)
parser.add_argument(
    "--development", dest="development", help="Pulls scripts from the .dev folder instead.",action="store_true"
)
parser.add_argument(
    "--no-syncscript", dest="nosyncscript", help="Don't sync the sync script", action="store_true"
)
args = parser.parse_args()
logger = logging.getLogger(__name__)

if args.debug:
    logging.basicConfig(level=logging.DEBUG)


def file_sha256(path: str) -> str:
    """
    Helper function for calculating sha256 of a file.

    Args:
        path (str): Path to file

    Raises:
        ValueError: Raised if path doesn't exist

    Returns:
        str: sha256 hash of file
    """
    if not os.path.exists(path):
        raise ValueError(f"{path} does not exist.")
    with open(path, "rb") as fi:
        data = fi.read()
        return hashlib.sha256(data).hexdigest()


INMON_SCRIPT_LOCATION = "/usr/lib/nagios/plugins"  # Script location for linux
hash_container = {}

# Create an archive folder if it doesn't exist already.
directory = os.path.join(INMON_SCRIPT_LOCATION, 'archive')
if not os.path.exists(directory):
    os.mkdir(directory)


# Get a container of hashes.
for script in os.listdir(INMON_SCRIPT_LOCATION):
    loc = os.path.join(INMON_SCRIPT_LOCATION, script)
    if os.path.isdir(loc):
        continue
    hash_container[script] = file_sha256(loc)

logger.debug("Items on local:\n %s\n\n", pprint.pformat(hash_container))

dev_location = ""
if args.development:
    dev_location = ".dev/"
    

# Get list of checksums
result = requests.get(
    f"https://inmonscripts.indigex.com/{dev_location}Linux/.checksums.txt", timeout=10
)
if not result.ok:
    inmon.CheckResult(
        3, f"Unable to obtain .checksums.txt from scripts website, {result.text}"
    ).process()
items_remote = json.loads(result.text)
items_remote_loc = {k: {"sha256": v, "location": "Linux"} for k, v in items_remote.items()}

# If device is a satellite, also get satellite items.
if args.satellite:
    result = requests.get(
        f"https://inmonscripts.indigex.com/{dev_location}Satellite/.checksums.txt", timeout=10
    )
    if not result.ok:
        inmon.CheckResult(
            3, f"Unable to obtain .checksums.txt from satellite website, {result.text}"
        ).process()
    satellite_items_remote = json.loads(result.text)
    satellite_items_remote_loc = {k: {"sha256": v, "location": "Satellite"} for k, v in satellite_items_remote.items()}
    items_remote_loc.update(satellite_items_remote_loc)
    items_remote.update(satellite_items_remote)
    
logger.debug("Items pulled from remote:\n %s\n\n", pprint.pformat(items_remote))
# Use the difference of sets to get the values that are in b but not in a
# This effectively gets us a list of things that are either remote and changed or remote and not local.

items_to_get = {
    k: {
        "sha256": v["sha256"],
        "location": v["location"]
    } for k, v in items_remote_loc.items()
    if k not in hash_container or hash_container[k] != v["sha256"]
}

logger.debug("Items to download:\n %s\n\n", pprint.pformat(items_to_get))
check_container = inmon.MultiActiveCheck()
num_items_successful = 0
num_items_failed = 0
differing_scripts = []
missing_scripts = []
if len(items_to_get) == 0:
    inmon.CheckResult(0, "No differing or missing scripts").process()
for file, values in items_to_get.items():
    if file == "sync_inmonscripts.py" and not args.nosyncscript:
        # Get desired sync time
        r = requests.get("https://inmonscripts.indigex.com/.sync", timeout=10)
        if not r.ok:
            raise RuntimeError(f"Failed to get .sync file: {r.status_code} - {r.text}")

        # If it's time to sync
        ct = int(time.time())
        st = int(r.text)
        logger.debug("Sync time got: %s", st)
        logger.debug("Current time: %s", ct)
        if st <= ct:
            # Get the actual file
            r = requests.get(
                f"https://inmonscripts.indigex.com/{dev_location}Linux/sync_inmonscripts.py",
                timeout=60,
            )
            file_loc = os.path.join(INMON_SCRIPT_LOCATION, "sync_inmonscripts.py")

            # Write to disk (not a problem because python files are compiled to pyc and run)
            if args.dry_run:
                logger.debug("Wrote sync file.")
            else:
                with open(file_loc, "wb") as f:
                    f.write(r.content)

                # chmod +x
                st = os.stat(file_loc)
                os.chmod(file_loc, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
                check_container.append(
                    inmon.CheckResult(
                        0, "Downloaded and installed new version of sync file"
                    )
                )
            num_items_successful += 1
        else:
            check_container.append(inmon.CheckResult(0, "Awaiting desired sync time"))
        continue

    # File exists remote and not locally.
    if file in items_remote_loc and file not in hash_container:
        logger.debug(
            "%s downloaded because it exists on remote and not locally.", file[0]
        )
        missing_scripts.append(file)
    # File exists locally, but hashes differ.
    elif file in hash_container and file in items_remote_loc and hash_container[file] != values["sha256"]:
        logger.debug("%s downloaded because remote hash differs from local", file[0])
        differing_scripts.append(file)
        fl = os.path.join(INMON_SCRIPT_LOCATION, file)
        afl = os.path.join(INMON_SCRIPT_LOCATION, 'archive', file)
        if not args.dry_run:
            if os.path.exists(afl) and os.path.isfile(afl):
                os.remove(afl)
            shutil.move(fl, afl)
        else:
            logger.debug("Moved %s to archive", file[0])

    # Get file via request
    r = requests.get(f"https://inmonscripts.indigex.com/{dev_location}{values['location']}/{file}", timeout=30)
    logger.debug("%s status code: %s", file[0], r.status_code)

    # Handle errors for HTTP request
    if not r.ok:
        check_container.append(
            inmon.CheckResult(2, f"Unable to download {file}: {r.status_code}")
        )
        num_items_failed += 1
        continue

    # Try to write to disk.
    try:
        if not args.dry_run:
            file_loc = os.path.join(INMON_SCRIPT_LOCATION, file)
            # Write to disk
            with open(file_loc, "wb") as w:
                w.write(r.content)
            # chmod +x
            st = os.stat(file_loc)
            os.chmod(file_loc, st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
        else:
            logger.info("Dry run: Wrote %s to disk", file)
        num_items_successful += 1
    except Exception as e:  # pylint: disable=broad-exception-caught
        check_container.append(inmon.CheckResult(2, f"Unable to write to file: {e}"))
        num_items_failed += 1
    logger.debug("\n")

# Count up items that failed and items that were successful.
if num_items_failed == 0:
    check_container.append(
        inmon.CheckResult(
            0,
            f"Succeeded in downloading {num_items_successful} files, failed to download {num_items_failed} files",
        )
    )
else:
    check_container.append(
        inmon.CheckResult(
            2,
            f"Succeeded in downloading {num_items_successful} files, failed to download {num_items_failed} files",
        )
    )
logger.debug("Differing scripts:\n%s\n", pprint.pformat(differing_scripts))
logger.debug("Missing scripts:\n%s", pprint.pformat(missing_scripts))
check_container.process()
