# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
"""
Provides a modal dialog box for the submission progress when submitting to
AWS Deadline Cloud
"""

from __future__ import annotations

import logging
from typing import Any, Optional

from qtpy.QtCore import Qt, Signal, QSize
from qtpy.QtGui import QCloseEvent, QFontMetrics
from qtpy.QtWidgets import (  # pylint: disable=import-error; type: ignore
    QApplication,
    QDialog,
    QDialogButtonBox,
    QFormLayout,
    QGroupBox,
    QHBoxLayout,
    QLabel,
    QPushButton,
    QProgressBar,
    QStyle,
    QTextEdit,
    QVBoxLayout,
    QWidget,
    QSizePolicy,
)

from .._utils import tr
from ...config import set_setting
from ....job_attachments.progress_tracker import ProgressReportMetadata
from ._job_submission_worker import JobSubmissionWorker

__all__ = ["SubmitJobProgressDialog"]

logger = logging.getLogger(__name__)


class SubmitJobProgressDialog(QDialog):
    """
    A modal dialog box for the submission progress while submitting a job bundle
    to AWS Deadline Cloud.

    This dialog uses a QThread-based worker (JobSubmissionWorker) to run the
    submission in the background while displaying progress updates.
    """

    # This signal is sent when the background thread raises an exception.
    submission_thread_exception = Signal(BaseException)

    # These signals are sent from the background thread
    submission_thread_print = Signal(str)
    submission_thread_hashing_progress = Signal(ProgressReportMetadata)
    submission_thread_upload_progress = Signal(ProgressReportMetadata)
    submission_thread_request_warning_dialog = Signal(str, bool)

    # This signal is sent when the background thread succeeds.
    submission_thread_succeeded = Signal(str)
    progress_window_closed = Signal()

    job_id: Optional[str] = None

    def __init__(self, parent: QWidget) -> None:
        super().__init__(parent=parent)

        self._warning_dialog_canceled = False
        self._submission_complete = False
        self._worker: Optional[JobSubmissionWorker] = None

        # Connect internal signals to handlers
        self.submission_thread_print.connect(self.handle_print)
        self.submission_thread_hashing_progress.connect(self.handle_hashing_thread_progress_report)
        self.submission_thread_upload_progress.connect(self.handle_upload_thread_progress_report)
        self.submission_thread_succeeded.connect(self.handle_create_job_thread_succeeded)
        self.submission_thread_request_warning_dialog.connect(self.handle_request_warning_dialog)
        self.submission_thread_exception.connect(self.handle_thread_exception)

        self._build_ui()

    def start_job_submission(
        self,
        job_bundle_dir: str,
        job_parameters: list[dict[str, Any]] = [],
        **kwargs,
    ) -> None:
        """
        Starts a job submission background thread and returns immediately. It wires up
        appropriate callbacks and then forwards all arguments.

        See the documentation for deadline.client.api.create_job_from_job_bundle for
        more details.
        """

        kwargs["job_bundle_dir"] = job_bundle_dir
        kwargs["job_parameters"] = job_parameters
        kwargs["from_gui"] = True
        kwargs["submitter_name"] = kwargs.get("submitter_name", "CustomGUI")

        # Create and configure the worker
        self._worker = JobSubmissionWorker(self)

        # Connect worker signals to dialog signals/handlers
        # Using Qt.QueuedConnection ensures thread-safe signal delivery
        self._worker.print_message.connect(self.submission_thread_print.emit, Qt.QueuedConnection)
        self._worker.hashing_progress.connect(
            self.submission_thread_hashing_progress.emit, Qt.QueuedConnection
        )
        self._worker.upload_progress.connect(
            self.submission_thread_upload_progress.emit, Qt.QueuedConnection
        )
        self._worker.confirmation_requested.connect(
            self._handle_confirmation_requested, Qt.QueuedConnection
        )
        self._worker.succeeded.connect(self._handle_worker_succeeded, Qt.QueuedConnection)
        self._worker.failed.connect(self._handle_worker_failed, Qt.QueuedConnection)

        # Set submission parameters and start
        self._worker.set_submission_kwargs(**kwargs)
        self._worker.start()

    def _handle_confirmation_requested(self, message: str, default_response: bool) -> None:
        """
        Handle confirmation request from worker thread.

        Shows the warning dialog and sends the result back to the worker.
        """
        if self._worker is None or self._worker.is_canceled:
            return

        # Show the dialog (this blocks until user responds)
        dialog = _JobSumissionWarningDialog(message, default_response, self)
        selection = dialog.exec()

        accepted = selection != QDialog.Rejected
        if not accepted:
            self._warning_dialog_canceled = True

        # Send result back to worker
        if self._worker is not None:
            self._worker.set_confirmation_result(accepted)

    def _handle_worker_succeeded(self, job_id: str) -> None:
        """Handle successful job submission from worker."""
        self.job_id = job_id
        self.submission_thread_succeeded.emit(job_id)

    def _handle_worker_failed(self, error: BaseException) -> None:
        """Handle failed job submission from worker."""
        self.submission_thread_exception.emit(error)

    def _build_ui(self):
        """Builds job submission progress UI"""
        # Remove help button from title bar
        self.setWindowFlags(
            (self.windowFlags() & ~Qt.WindowContextHelpButtonHint) | Qt.WindowCloseButtonHint
        )
        self.lyt = QVBoxLayout(self)
        self.lyt.setContentsMargins(5, 10, 5, 5)
        self.setMinimumWidth(600)
        self.setMinimumHeight(700)

        self.status_label = QLabel(tr("Preparing files..."))
        self.status_label.setMargin(5)
        self.hashing_progress = JobAttachmentsProgressWidget(
            initial_message=tr("Preparing for hashing..."),
            title=tr("Hashing progress"),
            parent=self,
        )
        self.upload_progress = JobAttachmentsProgressWidget(
            initial_message=tr("Preparing for upload..."), title=tr("Upload progress"), parent=self
        )
        self.submission_log = QTextEdit()
        self.submission_log.setReadOnly(True)
        self.button_box = QDialogButtonBox(Qt.Horizontal)
        self.button_box.setStandardButtons(QDialogButtonBox.Cancel)

        self.lyt.setAlignment(Qt.AlignTop)
        self.lyt.addWidget(self.status_label)
        self.lyt.addWidget(self.hashing_progress)
        self.lyt.addWidget(self.upload_progress)
        self.lyt.addWidget(self.submission_log)
        self.lyt.addWidget(self.button_box)

        self.setWindowTitle(tr("AWS Deadline Cloud submission"))

        self.button_box.accepted.connect(self.accept)
        self.button_box.rejected.connect(self.close)

    def handle_request_warning_dialog(self, message: str, default_response: bool):
        """Presents a dialog with the provided message.

        Args:
            message (str): The message to present to the user.
            default_response (bool):
                True if the default is to continue. This adds a "Do not ask again" button as well.
                False if the default is to Cancel.

        Note:
            This method is kept for backward compatibility with the signal-based approach.
            The actual handling is done in _handle_confirmation_requested.
        """
        # This is now handled by _handle_confirmation_requested
        pass

    def handle_print(self, message: str) -> None:
        """
        Handles the signal sent from the background thread to print messages
        to the log.
        """
        self.submission_log.append(f"{message}\n")

    def handle_hashing_thread_progress_report(
        self, progress_metadata: ProgressReportMetadata
    ) -> None:
        """
        Handles the signal sent from the background thread when reporting
        hashing progress. Sets the progress bar in the dialog based on
        the callback progress data from job attachments.
        """
        self.hashing_progress.progress_bar.setValue(int(progress_metadata.progress))
        self.hashing_progress.progress_message.setText(progress_metadata.progressMessage)

    def handle_upload_thread_progress_report(
        self, progress_metadata: ProgressReportMetadata
    ) -> None:
        """
        Handles the signal sent from the background thread when reporting
        upload progress. Sets the progress bar in the dialog based on
        the callback progress data from job attachments.
        """
        self.upload_progress.progress_bar.setValue(int(progress_metadata.progress))
        self.upload_progress.progress_message.setText(progress_metadata.progressMessage)

    def handle_create_job_thread_succeeded(self, job_id: str) -> None:
        """
        Handles the signal sent from the background CreateJob thread when the
        job creation has finished.
        """
        if job_id:
            self._submission_complete = True
            self.status_label.setText(tr("Submission complete"))
            self.button_box.setStandardButtons(QDialogButtonBox.Ok)
            self.button_box.button(QDialogButtonBox.Ok).setDefault(True)
            self.button_box.button(QDialogButtonBox.Ok).clicked.connect(
                self.progress_window_closed.emit
            )
        else:
            if self._is_canceled() or self._warning_dialog_canceled:
                self.status_label.setText(tr("Submission canceled"))
            else:
                self.status_label.setText(tr("Submission error"))
            self.button_box.setStandardButtons(QDialogButtonBox.Close)
            self.button_box.button(QDialogButtonBox.Close).setDefault(True)

    def _is_canceled(self) -> bool:
        """Check if the submission has been canceled."""
        return self._worker is not None and self._worker.is_canceled

    def handle_thread_exception(self, e: BaseException) -> None:
        """
        Handles the signal sent from the background threads when an exception is
        thrown.
        """
        self.button_box.setStandardButtons(QDialogButtonBox.Close)
        self.submission_log.append(f"Error occurred: {str(e)}\n")
        logger.exception(e, exc_info=(type(e), e, e.__traceback__))

    def closeEvent(self, event: QCloseEvent) -> None:
        """
        Overrides the closeEvent function to shutdown any running threads before
        closing the dialog. If the submission is complete then any button, even
        'X', should result in the dialog being accepted.
        """
        self.progress_window_closed.emit()
        if self._submission_complete:
            self.accept()
        else:
            if self._worker is not None:
                self._worker.cancel()
                logger.info("Canceling submission...")
                self.status_label.setText(tr("Canceling submission..."))
                # Wait for worker to finish with event processing
                while self._worker.isRunning():
                    QApplication.instance().processEvents()  # type: ignore[union-attr]
            super().closeEvent(event)

    def exec_(self) -> Optional[str]:  # type: ignore[override]
        """
        Runs the modal job progress dialog, returns the submitted job ID if it
        was successful, otherwise None.
        """
        if super().exec_() == QDialog.Accepted:
            return self.job_id
        return None


class JobAttachmentsProgressWidget(QGroupBox):
    """
    UI element to group job attachments progress bar with a status message.
    """

    def __init__(self, *, initial_message: str, title: str, parent: Optional[QWidget] = None):
        super().__init__(parent=parent, title=title)
        self.initial_message = initial_message

        self._build_ui()

    def _build_ui(self):
        self.layout = QFormLayout(self)
        self.layout.setFieldGrowthPolicy(QFormLayout.AllNonFixedFieldsGrow)

        self.progress_bar = QProgressBar()
        self.progress_message = QLabel(self.initial_message)

        self.layout.addWidget(self.progress_bar)
        self.layout.addWidget(self.progress_message)


class _JobSumissionWarningDialog(QDialog):
    """
    Simple Dialog which functions similar to a QMessageBox, but with a scrollable text area.
    """

    def __init__(
        self, message: str, default_response: bool = False, parent: Optional[QWidget] = None
    ):
        """
        Simple Dialog which functions similar to a QMessageBox, but with a scrollable text area.

        Args:
            message (str): The message to present to the user.
            default_response (bool):
                True if the default response should be to continue.
                    - This also adds a "Do not ask again" button which will set settings.auto_accept to True.
                False if the default response should be to Cancel.
        """
        super().__init__(parent=parent)
        self.setWindowTitle(tr("Job Submission Confirmation"))
        self.message = message
        self.default_response = default_response
        self.buttons = None
        layout = QVBoxLayout(self)

        # Top section with icon and title
        top_layout = QHBoxLayout()
        icon_label = QLabel()
        icon_label.setPixmap(
            self.style()
            .standardIcon(QStyle.SP_DirIcon if default_response else QStyle.SP_MessageBoxWarning)
            .pixmap(32, 32)
        )
        top_layout.addWidget(icon_label)
        title_label = QLabel(tr("Job submission confirmation"))
        title_label.setStyleSheet("font-weight: bold;")
        top_layout.addWidget(title_label)
        top_layout.addStretch()
        layout.addLayout(top_layout, 0)  # No stretch for title section

        # Scrollable Text Area
        self.text_edit = QTextEdit()
        self.text_edit.setPlainText(message)
        self.text_edit.setReadOnly(True)
        self.text_edit.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
        layout.addWidget(self.text_edit, 1)  # Stretch factor 1 for text area

        # Set minimum width and calculate optimal height
        self.text_edit.setMinimumWidth(500)
        self._calculate_optimal_size()

        self.buttons = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
        self.buttons.accepted.connect(self.accept)
        self.buttons.rejected.connect(self.reject)

        if default_response:
            # If the default response is to continue, add the "Do not ask again" button
            dont_ask_button = QPushButton(tr("Do not ask again"), self)
            dont_ask_button.clicked.connect(lambda: set_setting("settings.auto_accept", "true"))
            dont_ask_button.clicked.connect(self.accept)
            self.buttons.addButton(dont_ask_button, QDialogButtonBox.ActionRole)

        layout.addWidget(self.buttons)

    def _calculate_optimal_size(self):
        """Calculate optimal height based on text content and font metrics."""
        # Get font metrics for the text edit
        font_metrics = QFontMetrics(self.text_edit.font())

        # Calculate text width accounting for margins and scrollbar
        text_width = self.text_edit.minimumWidth() - 40  # Account for margins and scrollbar

        # Calculate the height needed for the text
        text_rect = font_metrics.boundingRect(0, 0, text_width, 0, Qt.TextWordWrap, self.message)
        text_height = text_rect.height()

        # Add some padding for better appearance
        padding = 20
        optimal_height = text_height + padding

        # Set reasonable bounds
        min_height = 100
        max_height = 500

        # Clamp the height to reasonable bounds
        final_height = max(min_height, min(optimal_height, max_height))

        self.text_edit.setMinimumHeight(min_height)
        # Set dialog's initial size based on content
        dialog_height = final_height + 120  # Add space for title and buttons
        self.resize(500, dialog_height)

    def showEvent(self, event):
        """Override showEvent to set the default button after the dialog is shown."""
        super().showEvent(event)

        # Set the default button after the dialog is fully shown
        if self.buttons is not None:  # This type check is to make linting pass
            if self.default_response:
                ok_button = self.buttons.button(QDialogButtonBox.Ok)
                if ok_button:
                    ok_button.setDefault(True)
                    ok_button.setAutoDefault(True)
                    ok_button.setFocus()
            else:
                cancel_button = self.buttons.button(QDialogButtonBox.Cancel)
                if cancel_button:
                    cancel_button.setDefault(True)
                    cancel_button.setAutoDefault(True)
                    cancel_button.setFocus()

    def sizeHint(self):
        """Return size hint based on content."""
        # Calculate total dialog height including other widgets
        base_height = 50  # Height for title, buttons, and margins
        text_height = self.text_edit.minimumHeight()
        total_height = base_height + text_height

        return QSize(500, total_height)
