import datetime
import json
import time
from http import HTTPStatus
from typing import Dict, Optional
import requests
from logger_wrapper import LoggerWrapper
from import_script_status_codes_translator import ImportScriptStatusCodeTranslator


class DuplicateDataCenterException(Exception):
    def __init__(self, ctm_name: str, message: str):
        super().__init__(message)
        self.ctm_name = ctm_name


class MigrationProcessManager:
    """
       The following class holds the logic to start the migration process
       and the logic that pulls the migration process status.
    """

    def __init__(self, service_base_url: str, s3_access_request_id_msg_prefix: str,
                 migration_request_id_msg_prefix: str, headers: Dict,
                 ctm_name: Optional[str] = None,
                 em_ctm_name: Optional[str] = None):
        self.start_import_service_url = service_base_url + "/import"
        self.poll_import_status_service_url = service_base_url + "/import/{requestId}"
        self.s3_access_request_id_msg_prefix = s3_access_request_id_msg_prefix
        self.migration_request_id_msg_prefix = migration_request_id_msg_prefix
        self.headers = headers
        self.ctm_name = ctm_name
        # Optional: when set (and not in per-server CTM mode), forwarded as
        # the ``ctmName`` HTTP header on the EM ``/import`` GET so the SaaS
        # backend treats the run as single-CTM mode against the inner EM TAR.
        self.em_ctm_name = em_ctm_name

    @property
    def _is_ctm_mode(self) -> bool:
        return bool(self.ctm_name)

    @staticmethod
    def _get_ctm_import_failure(polling_resp_content: Dict) -> Optional[str]:
        """
        Contract: for CTM, HTTP 200 only means the polling has ended; the actual
        outcome is carried by ``completed_status.return_code`` - ``0`` means the
        migration succeeded, any non-zero value means it failed for this CTM.
        """
        completed_status = polling_resp_content.get("completed_status") if isinstance(polling_resp_content, dict) else None
        if not isinstance(completed_status, dict):
            return None
        return_code = completed_status.get("return_code")
        if return_code is None or str(return_code) == "0":
            return None
        return str(completed_status.get("message") or "").strip() or f"return_code={return_code}"

    def start_migration_import_process(self, s3_access_request_id: str,
                                       skip_archive_rules: Optional[str] = None,
                                       skip_push_definition: Optional[str] = None,
                                       ctm_export_file_suffix: Optional[str] = None,
                                       data: Optional[str] = None):
        """
        The following method opens an HTTP connection to EM backend in order to start the migration import process.

        """
        message_prefix = f"[{self.s3_access_request_id_msg_prefix} {s3_access_request_id}]"
        try:
            if self._is_ctm_mode:
                request_kwargs = {
                    "url": self.start_import_service_url,
                    "headers": self.headers,
                    "params": {"userRequestId": s3_access_request_id},
                    "json": {
                        "ctm_name": self.ctm_name,
                        "ctm_export_file_suffix": ctm_export_file_suffix,
                    },
                }
                http_method = requests.post
            else:
                self.headers["s3AccessRequestId"] = s3_access_request_id
                self.headers["skipArchiveRules"] = skip_archive_rules
                self.headers["skipPushDefinition"] = skip_push_definition
                if data:
                    self.headers["data"] = data
                    print(f"MIGRATION DATA SCOPE - data: {data}")
                self.headers["unsupportedJobTypes"] = "'TANDEM','VMware','WS','MAPPER','OS2200','OS400','Cognos','DataStage','JAVA','MSG','BACKUP','SAP_BO','BladeLogic','RETAIL','AmazonEC2','Oracle_BI','z/OS','Report'"
                if self.em_ctm_name:
                    # EM-only single-CTM mode: sending the ``ctmName`` header
                    # on the EM ``/import`` GET makes the SaaS backend populate
                    # ImportFlag.CTM_NAME and dispatch to runSingleCtm() using
                    # the inner EM TAR (no per-server tar required).
                    self.headers["ctmName"] = self.em_ctm_name
                    LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                                      message=f"EM single-CTM mode header set: ctmName={self.em_ctm_name}",
                                                      logger_level='debug')
                request_kwargs = {"url": self.start_import_service_url, "headers": self.headers}
                http_method = requests.get


            LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                              message=f"Starting Control-M self-hosted data migration import process,"
                                                      f" service URL: {self.start_import_service_url}",
                                              logger_level='debug')
            http_response = http_method(**request_kwargs)
            LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                              message=f"HTTP returned status code for"
                                                      f" starting Control-M self-hosted data migration import process is: '{http_response.status_code}'",
                                              logger_level='debug')
            try:
                http_response_text_as_json = json.loads(http_response.text)
            except (ValueError, json.JSONDecodeError):
                http_response_text_as_json = None

            if http_response.status_code != HTTPStatus.OK or http_response_text_as_json is None:
                error_details = (http_response_text_as_json
                                 if http_response_text_as_json is not None
                                 else (http_response.text or '')[:500])
                LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                                  message=f"HTTP Failure - status={http_response.status_code} reason='{http_response.reason}'"
                                                          f" body: {error_details}",
                                                  logger_level="error")
                raise Exception(ImportScriptStatusCodeTranslator.FAILED_TO_START_MIGRATION_IMPORT_PROCESS.message)
            else:
                response_id_field = "session_id" if self._is_ctm_mode else "requestId"
                response_id = http_response_text_as_json.get(response_id_field)
                if not response_id:
                    LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                                      message=f"HTTP 200 OK but response body did not include the expected"
                                                              f" '{response_id_field}' field. body: {http_response_text_as_json}",
                                                      logger_level="error")
                    raise Exception(ImportScriptStatusCodeTranslator.FAILED_TO_START_MIGRATION_IMPORT_PROCESS.message)
                LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                                  message=f"Control-M self-hosted data migration server {response_id_field} is: {response_id}",
                                                  logger_level='debug')
                return response_id
        except Exception as e:
            LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                              message=f"Control-M self-hosted data migration process failed with exception message: {str(e)}",
                                              logger_level='error')
            raise Exception(ImportScriptStatusCodeTranslator.FAILED_TO_START_MIGRATION_IMPORT_PROCESS.message)

    def poll_migration_process_status(self, request_id: str,
                                      from_index: Optional[int] = None,
                                      to_index: Optional[int] = None):
        """
        The following method opens an HTTP connection to EM backend in order to poll the migration process current status.
        """
        message_prefix = f"[{self.migration_request_id_msg_prefix} {request_id}]"
        try:
            poll_url = self.poll_import_status_service_url.replace("{requestId}", request_id)
            LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                              message=f"Polling Control-M self-hosted data migration process status,"
                                                      f" service URL: {poll_url},"
                                                      f" from_index={from_index}, to_index={to_index}",
                                              logger_level='debug')
            headers = dict(self.headers)
            headers["requestId"] = request_id
            request_kwargs = {"url": poll_url, "headers": headers}
            if self._is_ctm_mode:
                headers["ctmName"] = self.ctm_name
                # The optional from/to are CTM-only (the EM endpoint does not
                # take them); forward them only when explicitly provided.
                params = {}
                if from_index is not None:
                    params["from"] = from_index
                if to_index is not None:
                    params["to"] = to_index
                if params:
                    request_kwargs["params"] = params
            http_response = requests.get(**request_kwargs)
            LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                              message=f"HTTP returned status code for polling"
                                                      f" Control-M self-hosted data migration process status is: '{http_response.status_code}'",
                                              logger_level='debug')
            LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                              message=http_response.text,
                                              logger_level='debug')

            http_response_text_as_json = json.loads(http_response.text)
            if http_response.status_code != HTTPStatus.OK and http_response.status_code != HTTPStatus.PARTIAL_CONTENT:
                LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                                  message=f"Failed to poll Control-M self-hosted data migration process status,"
                                                          f" Error Details: {http_response_text_as_json}",
                                                  logger_level='debug')

            return http_response.status_code, http_response_text_as_json
        except Exception as e:
            LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                              message=f"Control-M self-hosted data migration was failed"
                                                      f" with exception message: {str(e)}",
                                              logger_level='error')
            raise Exception(ImportScriptStatusCodeTranslator.FAILED_TO_POLL_MIGRATION_IMPORT_PROCESS_STATUS.message)

    def _extract_progress_messages(self, polling_resp_content: Dict) -> list:
        """
        Extract the ordered list of human-readable progress messages from a
        /import/{requestId} response. The wire shape differs between the
        EM and CTM endpoints:

          * EM   - {"progressMessages": ["Step 1/...", "Step 2/...", ...]}
                   (cumulative: every poll returns the full list so far)
          * CTM  - {"results": [{"line": "..."}, ...],
                    "completedStatus": {"message": "..."}}   # last poll only
                   (incremental: when the caller polls with ``from=N``, the
                    response contains only the new lines whose index is >= N)

        In CTM mode, ``results[*].line`` is read in order, and the optional
        ``completedStatus.message`` (only present on the terminating 200 OK
        poll) is appended as the final step.
        """
        if self._is_ctm_mode:
            messages = []
            for result in polling_resp_content.get("results") or []:
                if isinstance(result, dict):
                    line = result.get("line")
                    if line:
                        messages.append(line)
            completed_status = polling_resp_content.get("completedStatus")
            if isinstance(completed_status, dict):
                completed_msg = completed_status.get("message")
                if completed_msg:
                    messages.append(completed_msg)
            if messages:
                return messages

        return list(polling_resp_content.get("progressMessages") or [])

    def log_completed_steps(self, request_id: str, polling_resp_content: Dict, prev_completed_steps: set):
        """
        Log the new progress steps received in ``polling_resp_content`` and
        return the updated cumulative set of step indices logged so far.
        """
        message_prefix = f"[{self.migration_request_id_msg_prefix} {request_id}]"
        progress_messages_list = self._extract_progress_messages(polling_resp_content)

        base_index = len(prev_completed_steps) if self._is_ctm_mode else 0

        progress_messages = {}
        index = base_index + 1
        for message in progress_messages_list:
            progress_messages[index] = message
            index = index + 1

        LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                          message=f"progress_messages:\n{progress_messages}",
                                          logger_level='debug')
        steps_in_response = set(progress_messages.keys())

        # find new steps added since the last response:
        new_steps = steps_in_response - prev_completed_steps

        for step in sorted(new_steps):
            step_message = progress_messages[step]
            # skip just successful message of the final step:
            if step_message.startswith("Step -1/") and "successfully" in step_message:
                continue

            LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                              message=step_message)

        if self._is_ctm_mode:
            return prev_completed_steps | steps_in_response
        return steps_in_response

    def status_polling_handler(self, request_id):
        """
        The following method poll the migration status based on the input requestId to the backend server side.
        """
        prev_completed_steps = set()
        polling_sleep_intervals_in_seconds = 30
        polling_start_time = datetime.datetime.now()
        message_prefix = f"[{self.migration_request_id_msg_prefix} {request_id}]"
        next_from = 0 if self._is_ctm_mode else None
        while True:
            # Poll SaaS migration import process status:
            polling_resp_sc, polling_resp_content = self.poll_migration_process_status(
                request_id=request_id,
                from_index=next_from)

            if self._is_ctm_mode:
                new_lines_count = sum(
                    1 for r in (polling_resp_content.get("results") or [])
                    if isinstance(r, dict) and r.get("line")
                )
                next_from += new_lines_count

            # Partial result scenario:
            if polling_resp_sc == HTTPStatus.PARTIAL_CONTENT:
                prev_completed_steps = self.log_completed_steps(request_id=request_id,
                                                                polling_resp_content=polling_resp_content,
                                                                prev_completed_steps=prev_completed_steps)

                LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                                  message=f"The data migration from Control-M self-hosted to Control-M SaaS was not ended yet,"
                                                          f" next status polling will be in {polling_sleep_intervals_in_seconds} seconds.",
                                                  logger_level='debug')
                time.sleep(polling_sleep_intervals_in_seconds)

            # Success scenario:
            elif polling_resp_sc == HTTPStatus.OK:
                self.log_completed_steps(request_id=request_id,
                                         polling_resp_content=polling_resp_content,
                                         prev_completed_steps=prev_completed_steps)
                # For CTM, HTTP 200 only means polling ended. The real outcome is
                # in completed_status.return_code: 0 == success, non-zero == this
                # CTM's migration failed.
                if self._is_ctm_mode:
                    ctm_import_failure = self._get_ctm_import_failure(polling_resp_content)
                    if ctm_import_failure:
                        LoggerWrapper.log_message_elapsed_time(
                            elapsed_time_seconds=(datetime.datetime.now() - polling_start_time).total_seconds(),
                            message_prefix=message_prefix,
                            message="The data migration from Control-M self-hosted was failed"
                                    " to import to Control-M SaaS after",
                            message_log_level="error"
                        )
                        raise Exception(ImportScriptStatusCodeTranslator.CTM_IMPORT_FAILED.message.format(
                            ctm_name=self.ctm_name, detail=ctm_import_failure))
                return polling_start_time, message_prefix

            # Failure scenario:
            else:
                self.log_completed_steps(request_id=request_id,
                                         polling_resp_content=polling_resp_content,
                                         prev_completed_steps=prev_completed_steps)
                progress_messages_list = self._extract_progress_messages(polling_resp_content)
                for message in progress_messages_list:
                    if "already exists in def_tables" in message:
                        raise DuplicateDataCenterException(self.ctm_name or self.em_ctm_name, message)
                LoggerWrapper.log_message_elapsed_time(
                    elapsed_time_seconds=(datetime.datetime.now() - polling_start_time).total_seconds(),
                    message_prefix=message_prefix,
                    message="The data migration from Control-M self-hosted was failed"
                            " to import to Control-M SaaS after",
                    message_log_level="error"
                )
                raise Exception(ImportScriptStatusCodeTranslator.FAILED_WITH_INTERNAL_SERVER_ERROR.message)
