#!/usr/bin/env python3
# -*- coding: utf-8 -*-

###################
#    This tool runs CLI scripts and displays output in a Web Interface.
#    Copyright (C) 2021, 2022, 2023  Maurice Lambert

#    This program is free software: you can redistribute it and/or modify
#    it under the terms of the GNU General Public License as published by
#    the Free Software Foundation, either version 3 of the License, or
#    (at your option) any later version.

#    This program is distributed in the hope that it will be useful,
#    but WITHOUT ANY WARRANTY; without even the implied warranty of
#    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#    GNU General Public License for more details.

#    You should have received a copy of the GNU General Public License
#    along with this program.  If not, see <https://www.gnu.org/licenses/>.
###################

"""
This tool runs CLI scripts and displays output in a Web Interface.
"""

__version__ = "2.3.8"
__author__ = "Maurice Lambert"
__author_email__ = "mauricelambert434@gmail.com"
__maintainer__ = "Maurice Lambert"
__maintainer_email__ = "mauricelambert434@gmail.com"
__description__ = (
    "This tool runs CLI scripts and displays output in a Web Interface."
)
__license__ = "GPL-3.0 License"
__url__ = "https://github.com/mauricelambert/WebScripts"

copyright = """
WebScripts  Copyright (C) 2021, 2022, 2023  Maurice Lambert
This program comes with ABSOLUTELY NO WARRANTY.
This is free software, and you are welcome to redistribute it
under certain conditions.
"""
license = __license__
__copyright__ = copyright

print(copyright)

from os.path import join, exists, basename, splitext, split, abspath, dirname
from importlib.machinery import SourceFileLoader
from setuptools.command.install import install

from os import path, makedirs, getcwd, environ, sep
from setuptools import setup, find_packages
from getpass import getuser
from typing import Dict

import WebScripts as package
import importlib.util
import logging
import json
import sys
import os


arguments = [
    (
        "json-only",
        "j",
        "Delete the file named server.ini, to keep only the JSON"
        " configuration.",
    ),
    (
        "admin-password=",
        "p",
        'Administrator password for the WebScripts account "Admin"'
        " (the default account).",
    ),
    (
        "owner=",
        "o",
        "The owner of WebScripts Server (set the owner of files on "
        "UNIX systems if you are installing with privileges).",
    ),
    ("no-hardening", "n", "Do not harden during installation."),
    ("directory=", "d", "Current directory for the WebScripts server"),
]


class PostInstallScript(install):
    """
    This class installs and hardens the WebScripts project.
    """

    logging.basicConfig(
        filemode="w",
        filename="install.log",
        format="%(levelname)s\t::\t%(message)s",
        datefmt="%Y-%m-%d %H:%M:%S",
        level=logging.DEBUG,
        force=True,
    )
    logging.debug("Logging is configured")

    logging.debug("Add custom arguments")
    user_options = install.user_options + arguments

    logging.debug("System detection")
    is_windows = os.name == "nt"

    if is_windows:
        logging.warning("Permissions can not be change on Windows system.")
        logging.warning("Owner can not be change on Windows system.")

    def initialize_options(self):
        super(self.__class__, self).initialize_options()

        logging.debug("Initialize argument variables")
        self.json_only = False
        self.no_hardening = False
        self.admin_password = None
        self.directory = None
        self.owner = None

        logging.debug("Initialize custom properties")
        self.is_admin = None
        self.owner_property = None
        self.json_config_files = []
        self.ini_config_files = []
        self.py_scripts_files = []
        self.csv_files = []

    def linux_files_permissions(self, filename: str) -> None:
        """
        This function changes the owner and permissions on
        UNIX system files if you perform the installation
        with privileges.
        """

        logging.debug("Get directory...")
        self_directory = self.directory or getcwd()
        path_logs = join(self_directory, "logs")
        logging.info(f"Build the log directory {path_logs}...")
        makedirs(path_logs, exist_ok=True)

        filename1 = join(self_directory, "webscripts_file_integrity.json")
        if not exists(filename1):
            with open(filename1, "w") as file:
                file.write("{}")

        filename2 = join(self_directory, "uploads_file_integrity.json")
        if not exists(filename2):
            with open(filename2, "w") as file:
                file.write("{}")

        filename3 = join(self_directory, "logs_checks.json")
        if not exists(filename3):
            with open(filename3, "w") as file:
                file.write("{}")

        files = [
            join(self_directory, "audit." + extention)
            for extention in ("json", "txt", "html")
        ]
        for file in files:
            file = open(file, "w")
            file.close()

        if self.is_windows:
            return

        if self.is_admin is None:
            self.is_admin = os.getuid() == 0

        if not self.is_admin:
            logging.warning(
                "Permissions can not be change without privileges."
            )
            logging.warning("Owner can not be change without privileges.")

            return None

        if self.owner_property is None:
            from pwd import getpwnam

            logging.debug("Owner detection")
            owner = self.owner or getuser()
            logging.info(f"The owner will be {owner}")

            self.owner_property = getpwnam(owner)
            logging.info(f"UID will be {self.owner_property.pw_uid}")
            logging.info(f"GID will be {self.owner_property.pw_gid}")

        logging.debug(f"Change the owner of {filename}")
        os.chown(
            filename, self.owner_property.pw_uid, self.owner_property.pw_gid
        )

        logging.debug(f"Change the permissions of {filename}")

        file = split(filename)[1]
        extension = splitext(filename)[1]
        directory = dirname(filename)

        if file == "id" or extension == ".csv":
            os.chmod(filename, 0o600)
        else:
            os.chmod(filename, 0o400)

        logging.debug(
            f'Change permissions and owner on directory "{directory}"'
        )
        os.chmod(directory, 0o755)  # nosec
        os.chown(directory, 0, 0)

        if file == "WebScripts":
            logging.debug(
                f"Add the execution permissions for the owner on {filename}"
            )
            os.chmod(filename, 0o500)
        elif directory.endswith("data/uploads") or directory.endswith(
            "WebScripts/doc"
        ):
            logging.debug(f"Change owner for {directory} directory")
            os.chown(
                directory,
                self.owner_property.pw_uid,
                self.owner_property.pw_gid,
            )
            os.chmod(directory, 0o700)

            if directory.endswith("WebScripts/doc"):
                directory = directory[:-3] + "logs"
                os.makedirs(directory, exist_ok=True)
                os.chown(
                    directory,
                    self.owner_property.pw_uid,
                    self.owner_property.pw_gid,
                )
                os.chmod(directory, 0o700)

        logging.warning(f"Change permissions and owner of {self_directory}")
        os.chmod(self_directory, 0o755)  # nosec
        os.chown(self_directory, 0, 0)
        os.chown(
            path_logs, self.owner_property.pw_uid, self.owner_property.pw_gid
        )

        os.chown(
            filename1, self.owner_property.pw_uid, self.owner_property.pw_gid
        )
        os.chown(
            filename2, self.owner_property.pw_uid, self.owner_property.pw_gid
        )
        os.chown(
            filename3, self.owner_property.pw_uid, self.owner_property.pw_gid
        )
        os.chmod(filename1, 0o600)
        os.chmod(filename2, 0o600)
        os.chmod(filename3, 0o600)

        for file in files:
            os.chown(
                file, self.owner_property.pw_uid, self.owner_property.pw_gid
            )

    def add_absolute_paths_in_configuration(self) -> None:
        """
        This function adds absolute paths on configurations.
        """

        launcher = sys.executable
        for filename in self.json_config_files:
            logging.debug(f"Open and loads {filename}")
            with open(filename) as file:
                configurations: Dict[str, dict] = json.load(file)

            scripts = configurations.get("scripts")

            if scripts is None:
                logging.info("Configure specific configuration file")
                script = configurations.get("script")

                if script is not None:
                    logging.debug("Add the launcher")
                    script["launcher"] = launcher

                    if (
                        "build" + sep not in filename
                    ):  # if not in temp install directory
                        script_name, _ = splitext(basename(filename))
                        logging.info(f"Configure script named: {script_name}")
                        for py_filename in self.py_scripts_files:
                            if py_filename.endswith(f"{script_name}.py"):
                                logging.debug("Add the script absolute path.")
                                script["path"] = py_filename

                PostInstallScript.save_scripts_configurations(
                    filename, configurations
                )
                continue

            for name, section_name in scripts.items():
                logging.info(f"Configure {name}")
                section = configurations.get(section_name)
                specific_config_file = section.get("configuration_file")

                if specific_config_file:
                    specific_config_file = basename(specific_config_file)
                    for config_file in self.json_config_files:
                        config_file = abspath(config_file)
                        if (
                            config_file.endswith(specific_config_file)
                            and sep + "build" + sep not in config_file
                        ):
                            section["configuration_file"] = config_file
                    continue

                logging.debug("Add launcher")
                section["launcher"] = launcher

                if "build" + sep not in filename:
                    for py_filename in self.py_scripts_files:
                        if py_filename.endswith(name):
                            logging.debug("Add the script absolute path.")
                            section["path"] = py_filename

            server = configurations.get("server")

            if server is not None and "build" + sep not in filename:
                path_ = [dirname(filename), "..", "modules"]

                if self.is_windows:
                    path_.insert(1, "..")

                server["modules_path"] = abspath(join(*path_))

            PostInstallScript.save_scripts_configurations(
                filename, configurations
            )

    @staticmethod
    def save_scripts_configurations(
        filename: str, configurations: Dict[str, dict]
    ) -> None:
        """
        This function save configuration.
        """

        logging.debug(f"Save new/secure configurations in {filename}")
        with open(filename, "w") as file:
            json.dump(configurations, file, indent=4)

    def remove_configuration_files(self) -> None:
        """
        This function removes unnecessary configuration files.
        """

        sub_path = join("config", "nt")

        if not self.no_hardening or self.json_only:
            logging.info("Remove server.ini files")
            ini_config_files = self.ini_config_files.copy()

            for file in ini_config_files:
                os.remove(file)
                self.ini_config_files.remove(file)

        if self.no_hardening:
            return

        logging.debug("Research unused configuration files")
        if PostInstallScript.is_windows:
            unused_configurations = [
                f for f in self.json_config_files if sub_path not in f
            ]
        else:
            unused_configurations = [
                f for f in self.json_config_files if sub_path in f
            ]

        logging.info("Remove unused configuration files")
        for file in unused_configurations:
            logging.debug(f"Remove {file}.")
            os.remove(file)
            self.json_config_files.remove(file)

    def change_admin_password(self) -> None:
        """
        This function change the administrator
        password (default account named Admin).
        """

        if not self.admin_password:
            logging.warning(
                "The default administrator password is not changed (argument:"
                " --admin-password/-p is not used)."
            )
            return

        logging.debug("Import manage_defaults_databases (account manager)")
        module_name = "manage_defaults_databases"
        file_name = f"{module_name}.py"
        for filename in self.py_scripts_files:
            if filename.endswith(file_name):
                loader = SourceFileLoader(module_name, filename)
                break
        for filename in self.csv_files:
            if filename.endswith("users.csv"):
                environ["WEBSCRIPTS_DATA_PATH"] = split(filename)[0]
                break

        spec = importlib.util.spec_from_loader(module_name, loader)
        manage_defaults_databases = importlib.util.module_from_spec(spec)
        loader.exec_module(manage_defaults_databases)

        manage_defaults_databases.change_user_password(
            "2", self.admin_password
        )
        logging.info("Administrator is changed.")

    def run_custom_install(self) -> None:
        """
        This function launch custom install.
        """

        logging.basicConfig(
            filename="install.log",
            format="%(levelname)s\t::\t%(message)s",
            datefmt="%Y-%m-%d %H:%M:%S",
            level=logging.DEBUG,
            force=True,
        )
        logging.debug("Logging is configured")

        for filename in self.get_outputs():
            if not self.no_hardening:
                PostInstallScript.linux_files_permissions(self, filename)

            extension = splitext(filename)[1]

            if extension == ".json":
                self.json_config_files.append(filename)

            elif extension == ".py":
                self.py_scripts_files.append(filename)

            elif split(filename)[1] == "server.ini":
                self.ini_config_files.append(filename)

            elif extension == ".csv":
                self.csv_files.append(filename)

        PostInstallScript.remove_configuration_files(self)
        if not self.no_hardening:
            PostInstallScript.add_absolute_paths_in_configuration(self)

        PostInstallScript.change_admin_password(self)

    def run(self):
        return_value = super(self.__class__, self).run()
        PostInstallScript.run_custom_install(self)
        return return_value


setup(
    name=package.__name__,
    version=package.__version__,
    packages=find_packages(include=[package.__name__]),
    scripts=[
        join("Scripts", "wsgi.py"),
        join("Scripts", "activate_this.py"),
    ],
    author=package.__author__,
    author_email=package.__author_email__,
    maintainer=package.__maintainer__,
    maintainer_email=package.__maintainer_email__,
    description=package.__description__,
    long_description=open("README.md").read(),
    long_description_content_type="text/markdown",
    url=package.__url__,
    project_urls={
        "Documentation": "https://webscripts.readthedocs.io/en/latest/",
        "Wiki": "https://github.com/mauricelambert/WebScripts/wiki",
        "Presentation": (
            "https://www.slideshare.net/MauriceLambert"
            "1/webscripts-server-251581216"
        ),
    },
    include_package_data=True,
    classifiers=[
        "Programming Language :: Python",
        "Development Status :: 5 - Production/Stable",
        "Environment :: Web Environment",
        "Topic :: Internet :: WWW/HTTP :: WSGI :: Server",
        "Topic :: System :: Systems Administration",
        "Topic :: Communications :: File Sharing",
        "Topic :: Utilities",
        "Topic :: Security",
        "Operating System :: MacOS :: MacOS X",
        "Operating System :: Microsoft :: Windows",
        "Operating System :: POSIX :: Linux",
        "Natural Language :: English",
        "Programming Language :: Python :: 3.8",
        "Programming Language :: Python :: 3.9",
        "Programming Language :: Python :: 3.10",
        "Programming Language :: Python :: 3.11",
        "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
    ],
    keywords=[
        "Server",
        "Web",
        "Scripts",
        "SOC",
        "Administration",
        "DevOps",
        "WebScripts",
    ],
    platforms=["Windows", "Linux", "MacOS"],
    license=package.__license__,
    entry_points={
        "console_scripts": ["WebScripts = WebScripts:main"],
    },
    python_requires=">=3.9",
    cmdclass={
        # 'develop': PostDevelopScript,
        "install": PostInstallScript,
    },
)
