import argparse
import datetime
import gc
import json
import os
import shutil
import sys
import tarfile
import tempfile
import threading
from concurrent.futures import ThreadPoolExecutor, as_completed
from enum import Enum
from typing import List, Optional, Tuple

from export_file_validator import ExportFileValidator
from import_script_status_codes_translator import ImportScriptStatusCodeTranslator
from logger_wrapper import LoggerWrapper
from migration_process_manager import MigrationProcessManager
from migration_version_manager import EMUtilType, PackageTarContent
from s3_uploader import S3Uploader

# Valid values for --data (partial / scoped migration datasets).
MIGRATION_DATA_CHOICES = (
    'global_events',
    'services',
    'forecast',
    'ccp',
)


class RunMode(Enum):
    """
    The execution modes supported by the import script. The active mode is
    derived from the combination of ``--skip_em_migration``,
    ``--skip_ctm_migration``, ``--ctm_server`` and ``--em_ctm`` and drives
    every branching decision inside ``ImportScriptHandler.run()``.

      * :attr:`EM_THEN_CTMS` - default mode. Run the EM migration sequentially,
        then run all CTM migrations in parallel (bounded by ``max_parallel_ctms``).
        If EM fails the whole process fails immediately and no CTM is attempted.
        If a CTM fails the remaining CTMs keep running, and a final summary
        lists the failed ones.
      * :attr:`EM_ONLY` - run the EM migration only
        (``--skip_ctm_migration=true``).
      * :attr:`CTMS_ONLY` - skip EM, run all CTMs in parallel
        (``--skip_em_migration=true``).
      * :attr:`SINGLE_CTM` - skip EM and skip every other CTM, run only the CTM
        named in ``--ctm_server`` (per-server tar under ``servers/<name>/``).
      * :attr:`SINGLE_CTM_EM_ONLY` - run only the inner EM TAR scoped to a
        single CTM via ``--em_ctm``. Sends the ``ctmName`` header on the EM
        ``/import`` call so the backend dispatches to the single-CTM
        handlers. No per-server tar is required.

    Each member's ``value`` is the string that gets printed in the log
    (``run_mode=...``) so that operators see a stable, descriptive label
    rather than an opaque enum repr.
    """
    EM_THEN_CTMS = "em_then_ctms"
    EM_ONLY = "em_only"
    CTMS_ONLY = "ctms_only"
    SINGLE_CTM = "single_ctm"
    SINGLE_CTM_EM_ONLY = "single_ctm_em_only"


class ImportScriptHandler:
    """
        A class that manages and orchestrates the flow of the import script.
        The active execution mode is described by :class:`RunMode`.
    """

    def __init__(self, tenant_aapi_url: str, aapi_token: str, export_file_location: str, pre_sign_url_exp: int,
                 skip_tar_checksum_validation: bool, skip_archive_rules: str, skip_push_definition: str,
                 skip_ctm_migration: bool = False, skip_em_migration: bool = False,
                 single_ctm_name: Optional[str] = None, em_single_ctm_name: Optional[str] = None,
                 max_parallel_ctms: int = 2,
                 interactive_mode: bool = False,
                 data: Optional[list] = None, report_download_dir: str = 'logs'):
        self.tenant_aapi_url = tenant_aapi_url
        self.aapi_token = aapi_token
        self.export_file_location = export_file_location
        self.pre_sign_url_exp = pre_sign_url_exp
        self.skip_tar_checksum_validation = skip_tar_checksum_validation
        self.skip_archive_rules = skip_archive_rules
        self.skip_push_definition = skip_push_definition
        self.skip_ctm_migration = skip_ctm_migration
        self.skip_em_migration = skip_em_migration
        self.single_ctm_name = single_ctm_name.strip() if single_ctm_name else None
        self.em_single_ctm_name = em_single_ctm_name.strip() if em_single_ctm_name else None
        self.max_parallel_ctms = max_parallel_ctms
        self.interactive_mode = interactive_mode
        self.data = data
        self.report_download_dir = report_download_dir
        self.em_service_base_url = f"https://{self.tenant_aapi_url}/automation-api/migration/em"
        self.ctms_service_base_url = f"https://{self.tenant_aapi_url}/automation-api/migration/ctms"
        self.s3_access_request_id_msg_prefix = "s3-access-request-id:"
        self.migration_request_id_msg_prefix = "request-id:"
        self.headers = {
            'x-api-key': self.aapi_token,
            'Content-Type': 'application/json'
        }
        self._validate_run_mode_flags()

    def _validate_run_mode_flags(self):
        """
        Validate that the combination of ``--skip_em_migration``, ``--skip_ctm_migration``,
        ``--ctm_server`` and ``--em_ctm`` is consistent. Conflicting combinations are
        rejected up-front so that the user gets a clear error before any work is done.
        """
        if self.single_ctm_name and self.skip_ctm_migration:
            raise Exception("Conflicting flags: '--ctm_server' was provided but '--skip_ctm_migration=true' is set."
                            " Either remove '--ctm_server' or set '--skip_ctm_migration=false'.")
        if self.skip_em_migration and self.skip_ctm_migration and not self.single_ctm_name:
            raise Exception("Conflicting flags: both '--skip_em_migration=true' and '--skip_ctm_migration=true'"
                            " were provided - nothing would be migrated.")
        if self.em_single_ctm_name and self.single_ctm_name:
            raise Exception("Conflicting flags: '--em_ctm' and '--ctm_server' cannot be combined;"
                            " pick the EM-only single-CTM mode or the per-server single-CTM mode.")
        if self.em_single_ctm_name and self.skip_em_migration:
            raise Exception("Conflicting flags: '--em_ctm' requires the EM migration step;"
                            " remove '--skip_em_migration=true'.")
        if self.em_single_ctm_name and self.data:
            raise Exception("Conflicting flags: '--em_ctm' and '--data' are mutually exclusive.")
        if not isinstance(self.max_parallel_ctms, int) or self.max_parallel_ctms < 1:
            raise Exception(f"Invalid '--max_parallel_ctms' value: '{self.max_parallel_ctms}'."
                            f" It must be a positive integer.")

    @property
    def _run_mode(self) -> RunMode:
        if self.em_single_ctm_name:
            return RunMode.SINGLE_CTM_EM_ONLY
        if self.single_ctm_name:
            return RunMode.SINGLE_CTM
        if self.skip_em_migration:
            return RunMode.CTMS_ONLY
        if self.skip_ctm_migration:
            return RunMode.EM_ONLY
        return RunMode.EM_THEN_CTMS

    def log_migration_mode(self):
        """
        Log the migration mode based on whether a single-CTM flag is provided.
        """
        if self.em_single_ctm_name:
            LoggerWrapper.log_message_wrapper(
                message_prefix='',
                message=f"Running in SINGLE CTM EM-ONLY MODE - em_ctm={self.em_single_ctm_name},"
                        f" first_migration=False",
                logger_level='info'
            )
        elif self.single_ctm_name:
            LoggerWrapper.log_message_wrapper(
                message_prefix='',
                message=f"Running in SINGLE CTM MODE - ctm_server={self.single_ctm_name},"
                        f" first_migration=False",
                logger_level='info'
            )
        else:
            LoggerWrapper.log_message_wrapper(
                message_prefix='',
                message="Running in NORMAL MODE - No CTM name provided, first_migration=True",
                logger_level='info'
            )

    def run(self):
        # 1) Validate the input package TAR envelope:
        export_file_validator = ExportFileValidator(export_file_location=self.export_file_location,
                                                    skip_tar_checksum_validation=self.skip_tar_checksum_validation)
        export_file_validator.validate()

        extract_dir = tempfile.mkdtemp(prefix="saas_migration_import_")
        try:
            # 2) Extract the inner EM TAR + per-server CTM TARs:
            em_tar_location, ctm_inner_tars = self._extract_package_inner_tars(extract_dir=extract_dir)

            # 3) Display migration summary and get user confirmation:
            # Skip summary display in single CTM mode or data-scope mode
            if not self.em_single_ctm_name and not self.data:
                self.display_migration_summary(em_tar_location=em_tar_location, interactive=self.interactive_mode)

            # 4) Log start of the migration:
            LoggerWrapper.log_message_wrapper(message_prefix='', message="Starting migration of Control-M self-hosted data to Control-M SaaS.")
            total_start_time = datetime.datetime.now()
            LoggerWrapper.log_message_wrapper(message_prefix='', message=f"tenant_aapi_url={self.tenant_aapi_url}")
            LoggerWrapper.log_message_wrapper(message_prefix='', message=f"export_file_location={self.export_file_location}")
            run_mode = self._run_mode
            LoggerWrapper.log_message_wrapper(message_prefix='', message=f"run_mode={run_mode.value}")
            LoggerWrapper.log_message_wrapper(message_prefix='', message=f"max_parallel_ctms={self.max_parallel_ctms}")
            LoggerWrapper.log_message_wrapper(message_prefix='', message=f"pre_sign_url_exp={self.pre_sign_url_exp}", logger_level='debug')
            LoggerWrapper.log_message_wrapper(message_prefix='', message=f"skip_em_migration={self.skip_em_migration}", logger_level='debug')
            LoggerWrapper.log_message_wrapper(message_prefix='', message=f"skip_ctm_migration={self.skip_ctm_migration}", logger_level='debug')
            LoggerWrapper.log_message_wrapper(message_prefix='', message=f"single_ctm_name={self.single_ctm_name}", logger_level='debug')
            LoggerWrapper.log_message_wrapper(message_prefix='', message=f"em_single_ctm_name={self.em_single_ctm_name}", logger_level='debug')
            if self.data:
                LoggerWrapper.log_message_wrapper(message_prefix='', message=f"data={', '.join(self.data)}")

            # 5) Resolve which CTMs to migrate (based on the run mode):
            ctms_to_run = self._select_ctms_to_run(ctm_inner_tars=ctm_inner_tars, run_mode=run_mode)

            # 6) Run the EM portion (only when the run mode requires it). When
            # EM fails we abort before launching any CTM:
            if run_mode in (RunMode.EM_THEN_CTMS, RunMode.EM_ONLY, RunMode.SINGLE_CTM_EM_ONLY):
                ExportFileValidator.validate_inner_em_tar(em_tar_location=em_tar_location,
                                                          skip_tar_checksum_validation=self.skip_tar_checksum_validation)
                self._run_migration(tar_location=em_tar_location)
            else:
                LoggerWrapper.log_message_wrapper(message_prefix='',
                                                  message=f"Skipping EM migration (run_mode='{run_mode.value}').")

            # 7) Run the CTM portion (parallel) when applicable:
            if run_mode == RunMode.EM_ONLY:
                LoggerWrapper.log_message_wrapper(message_prefix='',
                                                  message="Skipping CTM migration based on optional input argument '--skip_ctm_migration'.")
            elif run_mode == RunMode.SINGLE_CTM_EM_ONLY:
                LoggerWrapper.log_message_wrapper(message_prefix='',
                                                  message=f"Skipping per-server CTM migration (run_mode='{run_mode.value}').")
            elif not ctms_to_run:
                LoggerWrapper.log_message_wrapper(message_prefix='',
                                                  message=f"No CTM export TAR files were found"
                                                          f" under '{PackageTarContent.get_servers_dir_prefix()}<ctmName>/' in the package TAR."
                                                          f" Skipping CTM migration.")
            else:
                self._run_ctm_migrations_in_parallel(ctms_to_run=ctms_to_run)

            LoggerWrapper.log_message_elapsed_time(
                elapsed_time_seconds=(datetime.datetime.now() - total_start_time).total_seconds(),
                message_prefix='',
                message="The data migration from Control-M self-hosted to Control-M SaaS took",
                message_suffix="to complete")
        finally:
            try:
                shutil.rmtree(extract_dir, ignore_errors=True)
            except Exception as cleanup_err:
                LoggerWrapper.log_message_wrapper(message_prefix='',
                                                  message=f"Failed to cleanup temp extract directory '{extract_dir}': {str(cleanup_err)}",
                                                  logger_level='debug')

    def _select_ctms_to_run(self, ctm_inner_tars: List[Tuple[str, str]], run_mode: RunMode) -> List[Tuple[str, str]]:
        """
        Resolve which ``(ctm_name, ctm_tar_location)`` entries should be
        migrated, based on the active run mode:

          * :attr:`RunMode.EM_ONLY`            -> ``[]``
          * :attr:`RunMode.SINGLE_CTM_EM_ONLY` -> ``[]`` (per-server tars are not used)
          * :attr:`RunMode.SINGLE_CTM`         -> only the entry whose name matches ``self.single_ctm_name``
          * everything else                    -> all CTMs found in the package TAR
        """
        if run_mode in (RunMode.EM_ONLY, RunMode.SINGLE_CTM_EM_ONLY):
            return []
        if run_mode == RunMode.SINGLE_CTM:
            available_names = [name for name, _ in ctm_inner_tars]
            matching = [(name, path) for name, path in ctm_inner_tars if name == self.single_ctm_name]
            if not matching:
                raise Exception(f"'--ctm_server={self.single_ctm_name}' was specified but no matching"
                                f" CTM export was found in the package TAR."
                                f" Available CTMs: {available_names if available_names else 'none'}.")
            return matching
        return list(ctm_inner_tars)

    def _run_ctm_migrations_in_parallel(self, ctms_to_run: List[Tuple[str, str]]):
        """
        Run every CTM migration in parallel, capped by ``self.max_parallel_ctms``.
        Failures of individual CTMs do NOT abort the others - they are collected
        and reported in a final summary. The method raises if at least one CTM
        failed so the script exits with a non-zero code.
        """
        worker_count = min(self.max_parallel_ctms, len(ctms_to_run))
        LoggerWrapper.log_message_wrapper(message_prefix='',
                                          message=f"Running {len(ctms_to_run)} CTM migration(s) in parallel"
                                                  f" with up to {worker_count} concurrent worker(s).")
        parallel_start_time = datetime.datetime.now()

        successes: List[str] = []
        failures: List[Tuple[str, str]] = []
        with ThreadPoolExecutor(max_workers=worker_count, thread_name_prefix="CTM-pool") as pool:
            future_to_ctm = {
                pool.submit(self._run_migration, tar_location=tar_path, ctm_name=name): name
                for name, tar_path in ctms_to_run
            }
            for future in as_completed(future_to_ctm):
                ctm_name = future_to_ctm[future]
                try:
                    future.result()
                    successes.append(ctm_name)
                    LoggerWrapper.log_message_wrapper(message_prefix='',
                                                      message=f"CTM migration for '{ctm_name}' completed successfully.")
                except Exception as e:
                    failures.append((ctm_name, str(e)))
                    LoggerWrapper.log_message_wrapper(message_prefix='',
                                                      message=f"CTM migration for '{ctm_name}' failed: {str(e)}",
                                                      logger_level='error')

        self._log_ctm_migrations_summary(successes=successes,
                                         failures=failures,
                                         elapsed_seconds=(datetime.datetime.now() - parallel_start_time).total_seconds())
        if failures:
            failed_names = ", ".join(name for name, _ in failures)
            raise Exception(f"{len(failures)} of {len(ctms_to_run)} CTM migration(s) failed: {failed_names}")

    @staticmethod
    def _log_ctm_migrations_summary(successes: List[str], failures: List[Tuple[str, str]], elapsed_seconds: float):
        """
        Print a clear, boxed summary of the parallel CTM migrations once they
        have all finished.
        """
        total = len(successes) + len(failures)
        minutes, seconds = divmod(int(elapsed_seconds), 60)
        elapsed_text = f"{minutes} minutes and {seconds} seconds" if minutes >= 1 else f"{seconds} seconds"
        title = " CTM Migrations Summary "
        line = "=" * 70
        lines = [
            "",
            line,
            title.center(70, "="),
            line,
            f"Total CTMs:     {total}",
            f"Successful:     {len(successes)}" + (f"  ({', '.join(successes)})" if successes else ""),
            f"Failed:         {len(failures)}" + (f"  ({', '.join(name for name, _ in failures)})" if failures else ""),
            f"Total elapsed:  {elapsed_text}",
        ]
        if failures:
            lines.append("")
            lines.append("Failed CTM details:")
            for name, err in failures:
                lines.append(f"  - {name}: {err}")
        lines.append(line)
        log_level = 'error' if failures else 'info'
        LoggerWrapper.log_message_wrapper(message_prefix='',
                                          message="\n".join(lines),
                                          logger_level=log_level)

    def _run_migration(self, tar_location: str, ctm_name: str = None):
        """
        Run the migration flow against the input export TAR. The same steps run
        for both EM and CTM migrations (only the backend URL and CTM import
        parameters differ when ``ctm_name`` is provided):

          1) Get S3 pre-signed URL (with the ``ctmName`` header in CTM mode).
          2) Upload the export TAR to the returned S3 URL.
          3) Start the migration via the appropriate ``/import`` endpoint.
          4) Poll status via the appropriate ``/import/<requestId>`` endpoint.

        The thread running this method is renamed to ``EM`` or
        ``CTM:<ctmName>`` for the duration of the call so that the log format
        (``[%(threadName)s]``) tags every log line with the owning migration
        (e.g. ``[EM]`` / ``[CTM:st-ntx-m0ugk]``). Concurrent CTM migrations
        therefore stay readable on a single stdout.

        When ``--em_ctm`` is set, ``self.em_single_ctm_name`` is forwarded as
        the ``ctmName`` HTTP header on the EM ``/import`` start call (but NOT
        to the S3 upload, which must still target the EM S3 path) so the
        backend dispatches to the single-CTM handlers while reading the inner
        EM TAR. ``ctm_name`` stays ``None`` in this mode.

        :param tar_location: file system path of the export TAR to upload.
        :param ctm_name:     when set, switches to CTM mode and uses the CTM
                              start/poll endpoints. When ``None``, runs the
                              existing EM migration flow.
        """
        current_thread = threading.current_thread()
        original_thread_name = current_thread.name
        current_thread.name = f"CTM:{ctm_name}" if ctm_name else "EM"
        try:
            # EM-only single-CTM mode is active only on the EM branch, never
            # when ctm_name is set (per-server tar mode). Resolve it locally
            # so the wire-call wiring below stays explicit.
            em_ctm_header_name = self.em_single_ctm_name if not ctm_name else None
            if ctm_name:
                LoggerWrapper.log_message_wrapper(message_prefix='',
                                                  message=f"Starting CTM migration for Control-M/Server '{ctm_name}'.")
                migration_service_base_url = self.ctms_service_base_url
            elif em_ctm_header_name:
                LoggerWrapper.log_message_wrapper(message_prefix='',
                                                  message=f"Starting EM migration scoped to single CTM '{em_ctm_header_name}'"
                                                          f" (ctmName header will be set on /import).")
                migration_service_base_url = self.em_service_base_url
            else:
                LoggerWrapper.log_message_wrapper(message_prefix='',
                                                  message=f"Starting EM migration.")
                migration_service_base_url = self.em_service_base_url

            # 1) + 2) Get S3 pre-signed URL and upload the TAR to S3. The EM
            # single-CTM mode must use the EM S3 path (NOT the per-CTM S3
            # path), so do NOT pass em_ctm_header_name to S3Uploader.
            s3_uploader = S3Uploader(service_base_url=self.em_service_base_url,
                                     s3_access_request_id_msg_prefix=self.s3_access_request_id_msg_prefix,
                                     headers=dict(self.headers),
                                     pre_sign_url_exp=self.pre_sign_url_exp,
                                     export_file_location=tar_location,
                                     ctm_name=ctm_name)
            get_s3_pre_signed_url_resp = s3_uploader.get_s3_pre_signed_url()
            pre_signed_url = get_s3_pre_signed_url_resp["preSignURL"]
            s3_access_request_id = get_s3_pre_signed_url_resp["s3AccessRequestId"]
            s3_uploader.upload_export_file_to_s3(s3_pre_signed_url=pre_signed_url,
                                                 s3_access_request_id=s3_access_request_id)

            ctm_export_file_suffix = S3Uploader.extract_export_file_suffix(pre_signed_url) if ctm_name else None

            # 3) + 4) Start the migration and poll its status:
            migration_process_manager = MigrationProcessManager(service_base_url=migration_service_base_url,
                                                                s3_access_request_id_msg_prefix=self.s3_access_request_id_msg_prefix,
                                                                migration_request_id_msg_prefix=self.migration_request_id_msg_prefix,
                                                                headers=dict(self.headers),
                                                                ctm_name=ctm_name,
                                                                em_ctm_name=em_ctm_header_name)
            # Data-scope (--data ccp/global_events/services/forecast) is an EM-side
            # concept; it must not be forwarded on the per-CTM (CTMS) path.
            data_param = ','.join(self.data) if (self.data and not ctm_name) else None
            request_id = migration_process_manager.start_migration_import_process(s3_access_request_id=s3_access_request_id,
                                                                                  skip_archive_rules=self.skip_archive_rules,
                                                                                  skip_push_definition=self.skip_push_definition,
                                                                                  ctm_export_file_suffix=ctm_export_file_suffix,
                                                                                  data=data_param)

            polling_start_time, message_prefix = migration_process_manager.status_polling_handler(request_id=request_id)

            # Download the migration report locally for inspection, then delete
            # it from S3 to avoid stale artefacts.
            # Download only for EM-side modes that publish a report via /report:
            #   * EM-only single-CTM (--em_ctm) — report key is the CTM name.
            #   * Data-scope (--data ...) — report key is "migration".
            # Per-server CTM migrations do not use this path (no report to fetch).
            if data_param:
                report_name = "migration"
            else:
                report_name = ctm_name or em_ctm_header_name
            if report_name and (self.em_single_ctm_name or self.data):
                local_path = s3_uploader.download_report(ctm_name=report_name, destination_dir=self.report_download_dir)
                if local_path:
                    s3_uploader.delete_report_from_s3(ctm_name=report_name)

            phase_label = f"CTM '{ctm_name}'" if ctm_name else "EM"
            LoggerWrapper.log_message_elapsed_time(
                elapsed_time_seconds=(datetime.datetime.now() - polling_start_time).total_seconds(),
                message_prefix=message_prefix,
                message=f"The {phase_label} migration took",
                message_suffix="to complete")
        finally:
            current_thread.name = original_thread_name

    def _extract_package_inner_tars(self, extract_dir: str):
        """
        Extract the inner EM TAR and per-server CTM TARs out of the package TAR
        into the given ``extract_dir``.

        :return: a tuple ``(em_inner_tar_path, ctm_inner_tars)`` where
                 ``ctm_inner_tars`` is a list of ``(ctm_name, ctm_tar_location)``
                 tuples for every CTM export found.
        """
        em_dir_prefix = PackageTarContent.get_em_dir_prefix()
        servers_prefix = PackageTarContent.get_servers_dir_prefix()
        sep = PackageTarContent.TAR_SEP.value

        em_inner_tar_path = None
        ctm_inner_tars = []
        seen_ctm_names = set()
        try:
            with tarfile.open(self.export_file_location, 'r') as tar_file_ptr:
                for member in tar_file_ptr.getmembers():
                    if not member.isfile():
                        continue

                    # Inner EM TAR: any *.tar.gz directly under em/ :
                    if em_inner_tar_path is None and member.name.startswith(em_dir_prefix):
                        relative_name = member.name[len(em_dir_prefix):]
                        if sep not in relative_name and PackageTarContent.is_inner_tar_name(relative_name):
                            em_inner_tar_path = self._extract_member_to_dir(tar_file_ptr=tar_file_ptr,
                                                                            member=member,
                                                                            target_dir=extract_dir,
                                                                            target_filename=os.path.basename(relative_name))
                            continue

                    # Inner CTM TARs: any *.tar.gz directly under servers/<ctmName>/ :
                    if member.name.startswith(servers_prefix) and PackageTarContent.is_inner_tar_name(member.name):
                        ctm_name, inner_basename = self._extract_ctm_name_and_basename(member_name=member.name)
                        if not ctm_name or not inner_basename:
                            LoggerWrapper.log_message_wrapper(message_prefix='',
                                                              message=f"Skipping malformed package TAR entry: '{member.name}'",
                                                              logger_level='warning')
                            continue
                        if ctm_name in seen_ctm_names:
                            LoggerWrapper.log_message_wrapper(message_prefix='',
                                                              message=f"Multiple TAR files found under 'servers/{ctm_name}/'."
                                                                      f" Ignoring extra entry '{member.name}' - only the first one is migrated.",
                                                              logger_level='warning')
                            continue
                        ctm_target_dir = os.path.join(extract_dir, "servers", ctm_name)
                        ctm_tar_location = self._extract_member_to_dir(tar_file_ptr=tar_file_ptr,
                                                                       member=member,
                                                                       target_dir=ctm_target_dir,
                                                                       target_filename=inner_basename)
                        ctm_inner_tars.append((ctm_name, ctm_tar_location))
                        seen_ctm_names.add(ctm_name)
        except Exception as e:
            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"Failed to extract inner TARs from package TAR: {str(e)}",
                                              logger_level='error')
            raise Exception(ImportScriptStatusCodeTranslator.FAILED_TO_EXTRACT_INNER_TAR_FROM_PACKAGE.message)

        if not em_inner_tar_path:
            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"The package TAR is missing an inner EM export TAR"
                                                      f" (any *.tar.gz file directly under '{em_dir_prefix}').",
                                              logger_level='error')
            raise Exception(ImportScriptStatusCodeTranslator.INVALID_EXPORT_FILE_DATA.message)

        return em_inner_tar_path, ctm_inner_tars

    @staticmethod
    def _extract_ctm_name_and_basename(member_name: str):
        """
        Given a TAR member name like ``servers/<ctmName>/<some-name>.tar.gz``
        return the tuple ``(<ctmName>, <some-name>.tar.gz)``. Both elements
        are empty strings if the member does not match the expected layout
        (file directly under a single server folder).
        """
        sep = PackageTarContent.TAR_SEP.value
        servers_prefix = PackageTarContent.get_servers_dir_prefix()
        if not member_name.startswith(servers_prefix):
            return "", ""
        relative_path = member_name[len(servers_prefix):]
        parts = relative_path.split(sep)
        if len(parts) != 2:
            return "", ""
        ctm_name, inner_basename = parts[0], parts[1]
        if not ctm_name or not inner_basename:
            return "", ""
        return ctm_name, inner_basename

    @staticmethod
    def _extract_member_to_dir(tar_file_ptr, member, target_dir: str, target_filename: str) -> str:
        """
        Extract a single TAR member into the given target directory under the
        provided filename and return the absolute path of the extracted file.
        """
        if not os.path.isdir(target_dir):
            os.makedirs(target_dir, exist_ok=True)
        target_path = os.path.join(target_dir, target_filename)
        extracted_obj = tar_file_ptr.extractfile(member)
        if extracted_obj is None:
            raise Exception(f"Could not extract '{member.name}' from package TAR")
        with open(target_path, 'wb') as out_f:
            shutil.copyfileobj(extracted_obj, out_f)
        return target_path

    def display_migration_summary(self, em_tar_location: str, interactive=True):
        """
        Extract dc.exp from the EM export TAR, parse the COMM table,
        and display a summary of Control-M/Servers to be migrated.

        Args:
            em_tar_location: Path to the EM export TAR.
            interactive: If True, prompt user for confirmation. If False, just display the summary.
        """
        try:
            with tarfile.open(em_tar_location, 'r:gz') as tar:
                # Find dc.exp member anywhere in the TAR:
                member = None
                for m in tar.getmembers():
                    if m.name.endswith(EMUtilType.DC.value):
                        member = m
                        break

                if not member:
                    LoggerWrapper.log_message_wrapper(message_prefix='',
                                                      message=f"Could not find {EMUtilType.DC.value} in TAR. Skipping summary display.",
                                                      logger_level='warning')
                    return

                f = tar.extractfile(member)
                if not f:
                    return

                content = f.read().decode('ISO-8859-1')

                # Parse content
                fld_delim = "\x1c\x1b\x1d"
                rec_delim = "\x1e\x1b\x1f"

                # More robust parsing: find all "COMM" records regardless of delimiters
                servers = []
                
                # Split by record delimiter if possible, otherwise just use the whole content
                parts = content.split(rec_delim)
                for part in parts:
                    # Within each part, look for "COMM"
                    if "COMM" in part:
                        # Find the first occurrence of "COMM" followed by fld_delim
                        start_idx = part.find("COMM" + fld_delim)
                        if start_idx != -1:
                            # Extract the record starting from COMM
                            record = part[start_idx:]
                            fields = record.split(fld_delim)
                            
                            if len(fields) > 2:
                                # Name (2), Type/Platform (27), Description (19 or 4), OS Type (6)
                                name = fields[2]
                                platform = fields[27] if len(fields) > 27 else ""
                                os_type = fields[6] if len(fields) > 6 else ""
                                desc = fields[19] if len(fields) > 19 and fields[19] else (fields[4] if len(fields) > 4 else "")

                                # Map platform
                                if os_type.upper() == 'MVS' or platform == 'M':
                                    type_label = "z/OS"
                                elif platform in ('D', '4', '1', '2'):
                                    type_label = "Distributed"
                                else:
                                    type_label = "Distributed"  # Default

                                # Provide a descriptive label for the SaaS summary
                                if type_label == "Distributed":
                                    desc_label = "SaaS Control-M/Server"
                                else:
                                    desc_label = "Self-Hosted Control-M for z/OS"

                                servers.append((name, type_label, desc_label))

                if not servers:
                    LoggerWrapper.log_message_wrapper(message_prefix='',
                                                      message="No Control-M/Servers found in dc.exp. Skipping summary display.",
                                                      logger_level='warning')
                    return

                # When --em_ctm is set, narrow the topology to just that one
                # Control-M/Server so the pre-flight summary matches what the
                # SaaS backend will actually import.
                if self.em_single_ctm_name:
                    filtered_servers = [s for s in servers if s[0] == self.em_single_ctm_name]
                    if not filtered_servers:
                        LoggerWrapper.log_message_wrapper(message_prefix='',
                                                          message=f"'--em_ctm={self.em_single_ctm_name}' was specified but"
                                                                  f" no matching Control-M/Server was found in the EM"
                                                                  f" export (dc.exp)."
                                                                  f" Available CTMs: {[s[0] for s in servers]}.",
                                                          logger_level='error')
                        raise Exception(f"'--em_ctm={self.em_single_ctm_name}' was specified but no matching"
                                        f" Control-M/Server was found in the EM export."
                                        f" Available CTMs: {[s[0] for s in servers]}.")
                    servers = filtered_servers

                # # Display table
                # if self.em_single_ctm_name:
                #     summary_msg = (f"\nOnce the Import is complete the following Control-M/Server"
                #                    f" (EM-only single-CTM mode, --em_ctm={self.em_single_ctm_name})"
                #                    f" will be available in SaaS (Tenant URL: {self.tenant_aapi_url}):\n")
                # else:
                #     summary_msg = f"\nOnce the Import is complete the following Control-M/Servers topology will be available in SaaS (Tenant URL: {self.tenant_aapi_url}):\n"
                # print(summary_msg)
                # header = ["Server", "Type", "Description"]

                # # Calculate column widths
                # col_widths = [len(h) for h in header]
                # for s in servers:
                #     for i, val in enumerate(s):
                #         col_widths[i] = max(col_widths[i], len(str(val)))

                # # Print table
                # border = "+" + "+".join(["-" * (w + 2) for w in col_widths]) + "+"
                # print(border)

                # header_row = "|" + "|".join([f" {header[i]:<{col_widths[i]}} " for i in range(len(header))]) + "|"
                # print(header_row)
                # print(border)

                # for s in servers:
                #     row = "|" + "|".join([f" {s[i]:<{col_widths[i]}} " for i in range(len(s))]) + "|"
                #     print(row)

                # print(border)

                # # Confirmation prompt (only if interactive mode)
                if interactive:
                    while True:
                        response = input("\nContinue Y/N? ").strip().upper()
                        if response == 'Y':
                            break
                        elif response == 'N':
                            print("Migration cancelled by user.")
                            sys.exit(0)
                        else:
                            print("Invalid input. Please enter Y or N.")
                else:
                    print("\nProceeding with migration (non-interactive mode)...")

        except Exception as e:
            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"Error displaying migration summary: {str(e)}",
                                              logger_level='warning')

    @staticmethod
    def strtobool(val):
        """Convert a string representation of truth to true (1) or false (0).

        True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values
        are 'n', 'no', 'f', 'false', 'off', and '0'.  Raises ValueError if
        'val' is anything else.
        """
        val = val.lower()
        if val in ('y', 'yes', 't', 'true', 'on', '1'):
            return True
        elif val in ('n', 'no', 'f', 'false', 'off', '0'):
            return False
        else:
            raise ValueError("invalid truth value %r" % (val,))


if __name__ == '__main__':
    log_file_ref_msg = None
    try:
        # Required command line arguments for the import script:
        parser = argparse.ArgumentParser(description="Control-M self-hosted data migration"
                                                     " into Control-M SaaS import script's args parser")
        parser.add_argument('--tenant_aapi_url', required=True, type=str, help="Control-M SaaS Tenant's AAPI url")
        parser.add_argument('--aapi_token', required=True, type=str, help="Control-M SaaS Tenant AAPI token")
        parser.add_argument('--export_file_location', required=True, type=str,
                            help="The file system location of the export file to be import")
        parser.add_argument('--data', required=False, default=None, nargs='+', choices=list(MIGRATION_DATA_CHOICES),
                            help="Optional migration dataset scope(s). One or more of: "
                                 "global_events (imports GLOBAL_COND, skips existing prefixes), "
                                 "services, forecast, ccp (imports DEF_CONF_ITEMS, skips existing ids). "
                                 "Multiple values are supported, e.g. --data ccp global_events. "
                                 "Relevant for Single CTM mode.")
        parser.add_argument('--python_exe', required=False, default='python3', type=str,
                            help="The python interpreter name")
        parser.add_argument('--pre_sign_url_exp', default='10', type=str,
                            help="Optional argument to set the expiration in minutes of the generated s3 pre sign url"
                                 " (default value is '10' minutes)")
        parser.add_argument('--log_level', default='debug', choices=['debug', 'info', 'warning', 'error'],
                            help="Script log file logger level (default: 'debug')")
        parser.add_argument('--logs_dir', default='logs', help="Script logs directory (default dir is 'logs')")
        parser.add_argument('--skip_tar_checksum_validation', default='false', type=str,
                            help="Skip export tar checksum value validation")
        parser.add_argument('--skip_archive_rules', default='false', type=str,
                            help="Skip import of Workload Archiving Rules")
        parser.add_argument('--skip_push_definition', default='false', type=str,
                            help="Skip Push Definition into Control-M SaaS CTM Servers")
        parser.add_argument('--skip_ctm_migration', default='false', type=str,
                            help="Skip the CTM (Control-M/Server) migration step (only run the EM migration"
                                 " from the package TAR).")
        parser.add_argument('--skip_em_migration', default='false', type=str,
                            help="Skip the EM migration step. The script will run only the CTM migrations"
                                 " from the package TAR (in parallel, bounded by '--max_parallel_ctms').")
        parser.add_argument('--ctm_server', default=None, type=str,
                            help="Run the migration only for the specified Control-M/Server name,"
                                 " using its per-server export TAR under 'servers/<name>/' inside the"
                                 " package TAR. Implies skipping the EM migration and skipping every"
                                 " other CTM in the package TAR. Cannot be combined with"
                                 " '--skip_ctm_migration=true' or '--em_ctm'.")
        parser.add_argument('--em_ctm', default=None, type=str,
                            help="Run a single-CTM migration using ONLY the inner EM TAR "
                                 "(no per-server CTM tarballs required). Sets the ctmName HTTP "
                                 "header on the EM /import call so the SaaS backend treats the run "
                                 "as single-CTM mode (isSingleCtmMode=true). Cannot be combined "
                                 "with '--ctm_server', '--skip_em_migration=true', or '--data'.")
        parser.add_argument('--max_parallel_ctms', default=2, type=int,
                            help="Maximum number of CTM migrations to run in parallel after the EM migration"
                                 " completes. Default is 2.")
        parser.add_argument('--interactive_mode', default='false', type=str,
                            help="Enable interactive mode with user confirmation prompt before migration")

        # Parse the command-line arguments:
        args = parser.parse_args()

        # Config the script's logger to log to stdout and to the log file.
        # The %(threadName)s placeholder makes every log line carry the owning
        # migration ('EM' or 'CTM[<ctmName>]') which is essential when CTM
        # migrations run in parallel:
        log_file_name = f"{args.logs_dir}{os.sep}control_m_self_hosted_data_import_into_control_m_saas_{datetime.datetime.now().strftime('%Y_%m_%d_%H_%M_%S')}.log"
        log_file_ref_msg = "Please refer to script's log file: '" + os.path.abspath(log_file_name) + "' for additional information."
        LoggerWrapper.init_logger(log_dir_name=args.logs_dir,
                                  log_level=args.log_level,
                                  log_format="%(asctime)s - %(levelname)s - [%(threadName)s] - %(message)s",
                                  log_file_name=log_file_name)

        # Access the parsed arguments:
        tenant_aapi_url = args.tenant_aapi_url
        aapi_token = args.aapi_token
        export_file_location = args.export_file_location
        data = args.data
        pre_sign_url_exp = args.pre_sign_url_exp
        skip_tar_checksum_validation = ImportScriptHandler.strtobool(args.skip_tar_checksum_validation)
        skip_archive_rules = args.skip_archive_rules
        skip_push_definition = args.skip_push_definition
        skip_ctm_migration = ImportScriptHandler.strtobool(args.skip_ctm_migration)
        skip_em_migration = ImportScriptHandler.strtobool(args.skip_em_migration)
        single_ctm_name = args.ctm_server
        em_single_ctm_name = args.em_ctm
        max_parallel_ctms = args.max_parallel_ctms
        interactive_mode = ImportScriptHandler.strtobool(args.interactive_mode)

        # start the import script:
        import_script_handler = ImportScriptHandler(tenant_aapi_url=tenant_aapi_url,
                                                    aapi_token=aapi_token,
                                                    export_file_location=export_file_location,
                                                    pre_sign_url_exp=pre_sign_url_exp,
                                                    skip_tar_checksum_validation=skip_tar_checksum_validation,
                                                    skip_archive_rules=skip_archive_rules,
                                                    skip_push_definition=skip_push_definition,
                                                    skip_ctm_migration=skip_ctm_migration,
                                                    skip_em_migration=skip_em_migration,
                                                    single_ctm_name=single_ctm_name,
                                                    em_single_ctm_name=em_single_ctm_name,
                                                    max_parallel_ctms=max_parallel_ctms,
                                                    interactive_mode=interactive_mode,
                                                    data=data,
                                                    report_download_dir=args.logs_dir)
        import_script_handler.run()
        print(log_file_ref_msg)
        gc.collect()
        exit(0)
    except Exception as e:
        err_msg = "Exception was occurred during Control-M self-hosted data migration into Control-M SaaS import script, " \
                  f"the exception message is: {str(e)}"
        if LoggerWrapper.is_logger_configured():
            LoggerWrapper.log_message_wrapper(message_prefix='', message=err_msg, logger_level='error')
        else:
            print(err_msg)

        print(log_file_ref_msg)
        gc.collect()
        exit(1)
