import hashlib
import os
import tarfile

from import_script_status_codes_translator import ImportScriptStatusCodeTranslator
from logger_wrapper import LoggerWrapper
from migration_version_manager import EMExportVersion, EMTarContent, PackageTarContent


class ExportFileValidator:
    """
        The following class holds the logic of export TAR file's validations.

        The script accepts a single TAR format: a *ctm-package* TAR that bundles
        an EM export and zero-or-more CTMS exports:

            ctm-package/
            ├── em/
            │   └── EMExportForSaaS.tar.gz
            └── servers/
                ├── ctmName/
                ├── ctmName2/
                └── .../
    """

    TAR = "tar"
    DOT_GZ = ".gz"
    TAR_GZ = TAR + DOT_GZ

    def __init__(self, export_file_location: str, skip_tar_checksum_validation: bool):
        self.export_file_location = export_file_location
        self.skip_tar_checksum_validation = skip_tar_checksum_validation

    def validate(self):
        """
        Validate the input package TAR envelope:

          1) The export file exists and is accessible.
          2) The file extension is ``.gz`` and the file is a valid TAR archive.
          3) The TAR contains an inner EM export TAR (any ``*.tar.gz`` file
             directly under ``em/``).
        """
        try:
            self._validate_file_is_valid_tar_gz()
            self._validate_package_tar_envelope()
        except Exception as e:
            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"Export TAR validation was failed with exception message: {str(e)}",
                                              logger_level='error')
            raise e

    def _validate_file_is_valid_tar_gz(self):
        """
        Check that the input file exists, ends with the ``.tar.gz`` suffix and
        is a valid (non-corrupted) TAR archive. Other extensions (including
        ``.tgz`` and bare ``.gz``) are not accepted.
        """
        if not os.path.isfile(self.export_file_location):
            raise Exception(ImportScriptStatusCodeTranslator.EXPORT_FILE_DOES_NOT_EXIST.message)

        expected_suffix = "." + ExportFileValidator.TAR_GZ  # ".tar.gz"
        LoggerWrapper.log_message_wrapper(message_prefix='',
                                          message=f"The export file path is: {self.export_file_location}",
                                          logger_level='debug')
        if not self.export_file_location.lower().endswith(expected_suffix):
            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"Export file must end with the '{expected_suffix}' suffix."
                                                      f" The actual export file is: '{self.export_file_location}'.",
                                              logger_level='error')
            raise Exception(ImportScriptStatusCodeTranslator.EXPORT_FILE_EXTENSION_IS_NOT_SUPPORTED.message)

        if not self.is_tar_gz_valid():
            raise Exception(ImportScriptStatusCodeTranslator.CORRUPTED_EXPORT_FILE.message)

    def _validate_package_tar_envelope(self):
        """
        Validate the outer envelope of a package TAR.
        """
        em_dir_prefix = PackageTarContent.get_em_dir_prefix()
        servers_dir_prefix = PackageTarContent.get_servers_dir_prefix()
        em_dir_name = PackageTarContent.EM_DIR.value
        servers_dir_name = PackageTarContent.SERVERS_DIR.value

        with tarfile.open(self.export_file_location, 'r') as tar_file_ptr:
            members = tar_file_ptr.getmembers()

        # 1) em/ must exist
        if not any(self._is_under_or_is_dir(m, em_dir_prefix, em_dir_name) for m in members):
            self._raise_missing_envelope_entry(
                f"The package {ExportFileValidator.TAR} is missing the top-level"
                f" '{em_dir_prefix}' directory.")

        # 2) servers/ must exist
        if not any(self._is_under_or_is_dir(m, servers_dir_prefix, servers_dir_name) for m in members):
            self._raise_missing_envelope_entry(
                f"The package {ExportFileValidator.TAR} is missing the top-level"
                f" '{servers_dir_prefix}' directory.")

        # 3) One *.tar.gz file directly under em/ must exist:
        if not any(self._is_inner_em_tar(m, em_dir_prefix) for m in members):
            self._raise_missing_envelope_entry(
                f"The package {ExportFileValidator.TAR} is missing an inner EM export TAR"
                f" (any *.tar.gz file directly under '{em_dir_prefix}').")

        LoggerWrapper.log_message_wrapper(message_prefix='',
                                          message=f"The package '{ExportFileValidator.TAR}' envelope is valid",
                                          logger_level='debug')

    @staticmethod
    def _is_under_or_is_dir(member, dir_prefix: str, dir_name: str) -> bool:
        """
        True if the given TAR member identifies the presence of a top-level
        directory.
        """
        name = member.name
        return name.startswith(dir_prefix) or (name == dir_name and member.isdir())

    @staticmethod
    def _is_inner_em_tar(member, em_dir_prefix: str) -> bool:
        """
        True if the given TAR member is a ``*.tar.gz`` file directly under
        ``em/``.
        """
        if not member.isfile():
            return False
        if not member.name.startswith(em_dir_prefix):
            return False
        relative_name = member.name[len(em_dir_prefix):]
        if PackageTarContent.TAR_SEP.value in relative_name:
            return False
        return PackageTarContent.is_inner_tar_name(relative_name)

    @staticmethod
    def _raise_missing_envelope_entry(specific_message: str):
        LoggerWrapper.log_message_wrapper(message_prefix='',
                                          message=f"{specific_message} The script accepts only the package"
                                                  f" TAR layout (em/<any-name>.tar.gz, servers/<ctmName>/<any-name>.tar.gz).",
                                          logger_level='error')
        raise Exception(ImportScriptStatusCodeTranslator.INVALID_EXPORT_FILE_DATA.message)

    @staticmethod
    def validate_inner_em_tar(em_tar_location: str, skip_tar_checksum_validation: bool):
        """
        Validate an EM export TAR that was extracted out of a package TAR.
        Runs the same structure / checksum checks that the historical EM-only
        format used to require.
        """
        validator = ExportFileValidator(export_file_location=em_tar_location,
                                        skip_tar_checksum_validation=skip_tar_checksum_validation)
        if not os.path.isfile(em_tar_location):
            raise Exception(ImportScriptStatusCodeTranslator.EXPORT_FILE_DOES_NOT_EXIST.message)
        if not validator.is_tar_gz_valid():
            raise Exception(ImportScriptStatusCodeTranslator.CORRUPTED_EXPORT_FILE.message)
        validator.validate_tar_content()

    def is_tar_gz_valid(self):
        """
        Validates that the TAR file is not corrupted.
        """
        try:
            return tarfile.is_tarfile(self.export_file_location)
        except Exception as e:
            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"Tar file is not a valid {ExportFileValidator.TAR} file. "
                                                      f"The following exception was caught while trying to check the file: {str(e)}",
                                              logger_level='error')
            return False

    def validate_tar_content(self):
        """
        The following method performs the following validations on the tar file content:
        1) Validates the TAR files structure
        2) Invoke method that Validate the checksum value that represents the TAR content
        """
        main_tar_dir = EMTarContent.EM_EXPORT_FOR_SAAS_MAIN_DIR.value
        main_tar_dir_prefix = EMTarContent.get_main_tar_dir_prefix()

        with tarfile.open(self.export_file_location, 'r') as tar_file_ptr:

            # retrieve the expected tar content to be validated based on the ExportScriptVersion.txt content:
            tar_em_export_version = ExportFileValidator.extract_file_content_from_tar(tar_file_ptr=tar_file_ptr,
                                                                                      file_name=f"{main_tar_dir_prefix}{EMTarContent.EXPORT_SCRIPT_VERSION.value}")

            if not tar_em_export_version:
                LoggerWrapper.log_message_wrapper(message_prefix='',
                                                  message=f"Tar file export version is missing or not supported",
                                                  logger_level='error')
                raise Exception(ImportScriptStatusCodeTranslator.INVALID_EXPORT_FILE_DATA.message)

            """
            Verify Export TAR version is compatible with the import version.
            """
            if tar_em_export_version != EMExportVersion.em_import_script_version.value:
                LoggerWrapper.log_message_wrapper(message_prefix='',
                                                  message=f"Export TAR version is not compatible with the import version: {EMExportVersion.em_import_script_version.value} {tar_em_export_version}",
                                                  logger_level='error')
                raise Exception(ImportScriptStatusCodeTranslator.INVALID_EXPORT_FILE_DATA.message)

            required_tar_members = EMExportVersion.get_tar_members_based_on_em_export_version(
                em_export_version=tar_em_export_version
            )

            if required_tar_members:
                # iterate over the main members of the main tar dir content:
                tar_members = tar_file_ptr.getmembers()
                tar_members_names = []
                for member in tar_members:
                    member_name = member.name
                    tar_members_names.append(member_name)

                    # validate the main tar dir is a directory:
                    if member_name == main_tar_dir and (not member.isdir()):
                        LoggerWrapper.log_message_wrapper(message_prefix='',
                                                          message=f"The {ExportFileValidator.TAR} file files structure is invalid. "
                                                                  f"The main {ExportFileValidator.TAR} dir: '{main_tar_dir}' is not a directory",
                                                          logger_level='error')
                        raise Exception(ImportScriptStatusCodeTranslator.INVALID_EXPORT_FILE_DATA.message)

                # validate that the required tar members exists in the expected list:
                for member in required_tar_members:
                    if member not in tar_members_names:
                        LoggerWrapper.log_message_wrapper(message_prefix='',
                                                          message=f"The {ExportFileValidator.TAR} file files structure is invalid. "
                                                                  f"The following {ExportFileValidator.TAR} member is missing: '{member}' inside the {ExportFileValidator.TAR}",
                                                          logger_level='error')
                        raise Exception(ImportScriptStatusCodeTranslator.INVALID_EXPORT_FILE_DATA.message)

                # validate the tar file checksum (based on env variable):
                if self.skip_tar_checksum_validation:
                    LoggerWrapper.log_message_wrapper(message_prefix='',
                                                      message=f"Skipping TAR checksum validation based on optional input argument '--skip_tar_checksum_validation'",
                                                      logger_level='info')
                else:
                    ExportFileValidator.validate_tar_checksum(tar_file_ptr=tar_file_ptr,
                                                              checksum_filename=f"{main_tar_dir_prefix}{EMTarContent.CHECKSUM.value}")

            # close the tar file descriptor:
            tar_file_ptr.close()

        # TAR validation was ended good:
        LoggerWrapper.log_message_wrapper(message_prefix='',
                                          message=f"The '{ExportFileValidator.TAR}' file content is valid",
                                          logger_level='debug')

    @staticmethod
    def validate_tar_checksum(tar_file_ptr, checksum_filename):
        """
        Validates the tar checksum value
        """
        export_hash_value = ExportFileValidator.extract_file_content_from_tar(tar_file_ptr=tar_file_ptr,
                                                                              file_name=checksum_filename)
        import_hash_value = ExportFileValidator.hash_tar_file_content(tar_file_ptr=tar_file_ptr,
                                                                      checksum_filename=checksum_filename)
        # compare the export checksum to the import checksum:
        if import_hash_value != export_hash_value:
            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"The {ExportFileValidator.TAR} file hash value is invalid: '{export_hash_value}'",
                                              logger_level='error')
            raise Exception(ImportScriptStatusCodeTranslator.INVALID_EXPORT_FILE_DATA.message)

    @staticmethod
    def hash_tar_file_content(tar_file_ptr, checksum_filename):
        """
        Hash a tar file content except the checksum.txt file
        """
        sha512_hash = hashlib.sha512()
        for member in tar_file_ptr.getmembers():
            file_obj = tar_file_ptr.extractfile(member)
            # check the hash of all the tar files except the checksum.txt and .log file:
            if file_obj and (member.name != checksum_filename) and (".log" not in member.name) and (
                    "Qualification" not in member.name):
                for chunk in iter(lambda: file_obj.read(4096), b''):
                    sha512_hash.update(chunk)
        return sha512_hash.hexdigest()

    @staticmethod
    def extract_file_content_from_tar(tar_file_ptr, file_name):
        """
        Extract specific file's content from tar file
        """
        for member in tar_file_ptr.getmembers():
            if member.name == file_name and member.isfile():
                file_obj = tar_file_ptr.extractfile(member)
                if file_obj:
                    return file_obj.read().decode('utf-8')
