import datetime
import json
import os
import threading
from http import HTTPStatus
from typing import Dict, Optional
from urllib.parse import unquote, urlparse

import requests

from import_script_status_codes_translator import ImportScriptStatusCodeTranslator
from logger_wrapper import LoggerWrapper


class S3Uploader:
    """
        The following class purpose is to get S3 pre-signed URL in order to
        upload the export TAR file to S3.

        When ``ctm_name`` is provided the request is scoped to the CTM-specific
        S3 object key (Control-M/Server export). Without it, the EM export key
        is used.
    """

    _PRE_SIGN_URL_LOCK = threading.Lock()

    def __init__(self, service_base_url: str, s3_access_request_id_msg_prefix: str, headers: Dict,
                 pre_sign_url_exp: int, export_file_location: str, ctm_name: Optional[str] = None):
        self.s3_pre_sign_url = service_base_url + "/s3access"
        self.s3_access_request_id_msg_prefix = s3_access_request_id_msg_prefix
        self.headers = headers
        self.pre_sign_url_exp = pre_sign_url_exp
        self.export_file_location = export_file_location
        self.ctm_name = ctm_name

    def get_s3_pre_signed_url(self):
        """
        The following method opens an HTTP connection to EM backend in order to retrieve
        S3 pre-signed URL for the export tar file to be uploaded.

        Concurrent CTM migrations call this from multiple threads, but the
        backend serializes presign requests and rejects overlaps with HTTP
        409. The HTTP call is therefore wrapped in ``_PRE_SIGN_URL_LOCK``
        so threads queue up briefly here; the upload that follows still
        runs in parallel.
        """
        try:
            self.headers["preSignURLExpInMin"] = str(self.pre_sign_url_exp)
            if self.ctm_name:
                self.headers["ctmName"] = self.ctm_name
            with self._PRE_SIGN_URL_LOCK:
                LoggerWrapper.log_message_wrapper(message_prefix='',
                                                  message=f"Starting to get S3 pre-signed URL, service url: {self.s3_pre_sign_url}",
                                                  logger_level='debug')
                http_response = requests.get(url=self.s3_pre_sign_url, headers=self.headers)
                LoggerWrapper.log_message_wrapper(message_prefix='',
                                                  message=f"HTTP returned status code for get S3 pre-signed URL request is: '{http_response.status_code}'",
                                                  logger_level='debug')
        except Exception as e:
            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"Get S3 pre-signed URL was failed with exception message: {str(e)}",
                                              logger_level='error')
            raise Exception(ImportScriptStatusCodeTranslator.FAILED_TO_GET_PRE_SIGNED_URL.message)

        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 and http_response_text_as_json is not None:
            return http_response_text_as_json

        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=f"HTTP Failure - status={http_response.status_code} reason='{http_response.reason}'"
                                                  f" body: {error_details}",
                                          logger_level='error')
        raise Exception(self._resolve_failure_message(http_response.text, http_response.reason))

    def _resolve_failure_message(self, response_text: str, response_reason: str) -> str:
        """
        Build the exception message for a failed ``/s3access`` response.

        For a CTM-scoped request, surface a single generic message that lists the
        possible causes (not a Control-M SaaS server, server missing/renamed in
        SaaS, invalid AWS credentials, ...) and appends the raw server response
        for reference. For the EM request keep the generic pre-signed URL message.
        """
        if not self.ctm_name:
            return ImportScriptStatusCodeTranslator.FAILED_TO_GET_PRE_SIGNED_URL.message

        detail = (response_text or '').strip()[:500] or (response_reason or "unknown error")
        return ImportScriptStatusCodeTranslator.CTM_MIGRATION_NOT_ALLOWED.message.format(
            ctm_name=self.ctm_name, detail=detail)

    def upload_export_file_to_s3(self, s3_pre_signed_url: str, s3_access_request_id: str):
        """
        The following method opens an HTTP connection with AWS S3 in order to upload the export
        tar file using the retrieved S3 pre-signed URL.
        """
        upload_start_time = datetime.datetime.now()
        message_prefix = f"[{self.s3_access_request_id_msg_prefix} {s3_access_request_id}]"
        try:
            LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                              message=f"Uploading the Control-M self-hosted migration data file '{self.export_file_location}'.")

            with open(self.export_file_location, 'rb') as file:
                http_response = requests.put(url=s3_pre_signed_url, data=file)
                LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                                  message=f"HTTP returned status code for uploading Control-M self-hosted migration data file to S3 is: '{http_response.status_code}'",
                                                  logger_level='debug')

                if http_response.status_code != HTTPStatus.OK:
                    LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                                      message=f"HTTP failure reason for uploading Control-M self-hosted migration data file to s3 is: '{http_response.reason}'",
                                                      logger_level='error')
                    raise Exception(ImportScriptStatusCodeTranslator.FAILED_TO_UPLOAD_EXPORT_FILE.message)

                LoggerWrapper.log_message_elapsed_time(
                    elapsed_time_seconds=(datetime.datetime.now() - upload_start_time).total_seconds(),
                    message_prefix=message_prefix,
                    message=f"Upload completed."
                            f" The Control-M self-hosted migration data file is '{S3Uploader.get_file_size_in_mb(file_path=self.export_file_location)}' MB"
                            f" and took",
                    message_suffix='to upload'
                )
        except Exception as e:
            LoggerWrapper.log_message_elapsed_time(
                elapsed_time_seconds=(datetime.datetime.now() - upload_start_time).total_seconds(),
                message_prefix=message_prefix,
                message=f"Upload Control-M self-hosted migration data file to s3 failed after",
                message_log_level='error'
            )
            LoggerWrapper.log_message_wrapper(message_prefix=message_prefix,
                                              message=f"The exception message: {str(e)}",
                                              logger_level="error")
            raise Exception(ImportScriptStatusCodeTranslator.FAILED_TO_UPLOAD_EXPORT_FILE.message)

    def download_report(self, ctm_name: str, destination_dir: str) -> str:
        """
        Get a pre-signed download URL for the Single CTM import report from the server,
        then download the file to destination_dir.
        Returns the local path of the downloaded file, or None on failure.
        """
        try:
            report_url_endpoint = self.s3_pre_sign_url.replace("/s3access", "/report")
            self.headers["ctmName"] = ctm_name
            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"Requesting import report download URL for ctmName: '{ctm_name}'")
            http_response = requests.get(url=report_url_endpoint, headers=self.headers)
            if http_response.status_code != HTTPStatus.OK:
                raise Exception(f"Failed to get report download URL from '{report_url_endpoint}', status: {http_response.status_code}, reason: {http_response.reason}")

            download_url = json.loads(http_response.text).get("reportDownloadUrl")
            if not download_url:
                raise Exception("reportDownloadUrl missing from server response")

            report_response = requests.get(url=download_url)
            if report_response.status_code != HTTPStatus.OK:
                raise Exception(f"Failed to download report from S3, status: {report_response.status_code}")

            today = datetime.date.today().strftime("%d_%m_%Y")
            file_name = f"migration_saas_report_{ctm_name}_{today}.log"
            os.makedirs(destination_dir, exist_ok=True)
            local_path = os.path.join(destination_dir, file_name)
            with open(local_path, 'wb') as f:
                f.write(report_response.content)

            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"Import report downloaded to: '{local_path}'")
            return local_path
        except Exception as e:
            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"Failed to download import report: {str(e)}",
                                              logger_level='warning')
            return None

    def delete_report_from_s3(self, ctm_name: str) -> bool:
        """
        Delete the import report for ``ctm_name`` from S3 via
        ``DELETE /automation-api/migration/em/report`` with the ``ctmName`` header.
        Returns True on success, False on failure (failure is logged as a warning,
        never raised — a missing cleanup must not abort the overall flow).
        """
        try:
            report_url_endpoint = self.s3_pre_sign_url.replace("/s3access", "/report")
            headers = dict(self.headers)
            headers["ctmName"] = ctm_name
            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"Deleting import report from S3 for ctmName: '{ctm_name}'")
            http_response = requests.delete(url=report_url_endpoint, headers=headers)
            if http_response.status_code not in (200, 204):
                raise Exception(
                    f"DELETE /report returned status {http_response.status_code}: {http_response.reason}"
                )
            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"Import report deleted from S3 for ctmName: '{ctm_name}'")
            return True
        except Exception as e:
            LoggerWrapper.log_message_wrapper(message_prefix='',
                                              message=f"Failed to delete import report from S3: {str(e)}",
                                              logger_level='warning')
            return False

    @staticmethod
    def get_file_size_in_mb(file_path: str):
        """
        return the file under the input file path in MB
        """
        return "{:.3f}".format(os.path.getsize(file_path) / (1024 * 1024))

    @staticmethod
    def extract_export_file_suffix(pre_signed_url: str) -> str:
        """
        Extract the bucket-relative export-file suffix from an S3 pre-signed URL.

        The backend builds the S3 object key as
        ``<EXT_TENANT_BUCKET_RELATIVE_NAME>/migration/<EM|CTMS>/.../*.tar.gz`` and
        returns a pre-signed URL whose path is the same key prefixed with ``/``.
        Returned value is the part starting at ``/migration/``, e.g.::

            /migration/CTMS/IN01/CTMSExportForSaaS.tar.gz

        """
        path = unquote(urlparse(pre_signed_url).path)
        marker = "/migration/"
        idx = path.rfind(marker)
        if idx == -1:
            raise Exception(f"Cannot extract export file suffix from S3 pre-signed URL"
                            f" - missing '{marker}' segment in path '{path}'")
        return path[idx:]
