# Copyright 2015-2025 Earth Sciences Department, BSC-CNS 
# 
# This file is part of Autosubmit. 
# 
# Autosubmit is free software: you can redistribute it and/or modify 
# it under the terms of the GNU General Public License as published by 
# the Free Software Foundation, either version 3 of the License, or 
# (at your option) any later version. 
# 
# Autosubmit is distributed in the hope that it will be useful, 
# but WITHOUT ANY WARRANTY; without even the implied warranty of 
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the 
# GNU General Public License for more details. 
# 
# You should have received a copy of the GNU General Public License 
# along with Autosubmit.  If not, see <http://www.gnu.org/licenses/>.

import email.utils
import re
import smtplib
import zipfile
from email.mime.application import MIMEApplication
from email.mime.multipart import MIMEMultipart
from email.mime.text import MIMEText
from pathlib import Path
from tempfile import TemporaryDirectory
from textwrap import dedent
from typing import TYPE_CHECKING

from autosubmit.config.basicconfig import BasicConfig
from autosubmit.log.log import AutosubmitError, Log

if TYPE_CHECKING:
    from autosubmit.platforms.platform import Platform


def _compress_file(
        temporary_directory: TemporaryDirectory,
        file_path: Path) -> Path:
    """Compress a file.

    The file is created inside the given temporary directory.

    The function returns a ``Path`` of the archive file.

    :param temporary_directory: The temporary directory.
    :type temporary_directory: TemporaryDirectory
    :param file_path: The path of the file to be compressed.
    :type file_path: Path
    :return: The Path object of the compressed file.
    :rtype: str
    :raises AutosubmitError: The file cannot be compressed.
    """
    try:
        zip_file_name = Path(temporary_directory.name, f'{file_path.name}.zip')
        with zipfile.ZipFile(zip_file_name, 'w', zipfile.ZIP_DEFLATED) as zip_file:
            zip_file.write(file_path, Path(file_path).name)
            return Path(zip_file.filename)
    except ValueError as e:
        raise AutosubmitError(
            code=6011,
            message='An error has occurred while compressing log files for a warning email',
            trace=str(e))


def _attach_file(file_path: Path, message: MIMEMultipart) -> None:
    """Attach a file to a message.

    The attachment file name will be the same name of the ``file_path``.

    :param file_path: The path of the file to be attached.
    :type file_path: Path
    :param message: The message for the file to be attached to.
    :type message: MIMEMultipart
    :raises AutosubmitError: The file cannot be attached.
    """
    try:
        compressed_file_name = file_path.name
        part = MIMEApplication(
            file_path.read_bytes(),
            Name=compressed_file_name,
            Content_disposition=f'attachment; filename="{compressed_file_name}'
        )
        message.attach(part)
    except (TypeError, ValueError) as e:
        raise AutosubmitError(
            code=6011,
            message='An error has occurred while attaching log files to a warning email about remote_platforms',
            trace=str(e))


def _generate_message_text(
        exp_id: str,
        job_name: str,
        prev_status: str,
        status: str) -> str:
    """Generate email body to notify about status change.

    :param exp_id: The experiment id.
    :type exp_id: str
    :param job_name: The name of the job.
    :type job_name: str
    :param prev_status: The previous status.
    :type prev_status: str
    :param status: The current status.
    :type status: str
    :return: The body of the email message.
    """
    return dedent(f'''\
        Autosubmit notification\n
        -------------------------\n\n
        Experiment id:  {exp_id}\n\n
        Job name: {job_name}\n\n
        The job status has changed from: {prev_status} to {status}\n\n\n\n\n
        INFO: This message was auto generated by Autosubmit,
        remember that you can disable these messages on Autosubmit config file.\n''')


def _generate_message_experiment_status(
        exp_id: str, platform: "Platform") -> str:
    """Generate email body for the experiment status notification.

    :param exp_id: The experiment id.
    :type exp_id: str
    :param platform: The platform.
    :type platform: Platform
    :return: The email body for the experiment status notification.
    :rtype: str
    """
    return dedent(f'''\
        Autosubmit notification: Remote Platform issues\n
        -------------------------\n
        Experiment id: {exp_id}
        Logs and errors: {BasicConfig.expid_aslog_dir(exp_id)}
        Attached to this message you will find the related _run.log files.\n
        Platform affected: {platform.name} using as host: {platform.host}\n
        [WARN] Autosubmit encountered an issue with a remote platform.
        It will resume itself, whenever is possible
        If this issue persists, you can change the host IP or put multiple hosts in the platform.yml file\n\n\n\n\n
        INFO: This message was auto generated by Autosubmit,
        remember that you can disable these messages on Autosubmit config file.\n''')


def _check_mail_address(mail_to: list[str]) -> None:
    if not isinstance(mail_to, list):
        raise ValueError(
            'Recipients of mail notifications must be a list of emails!')
    elif not mail_to:
        raise ValueError('Empty recipient list')
    elif any([not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', mail) for mail in mail_to]):
        raise ValueError('Invalid email in recipient list')


class MailNotifier:
    def __init__(self, basic_config):
        self.config = basic_config

    def notify_experiment_status(
            self,
            exp_id: str,
            mail_to: list[str],
            platform: "Platform") -> None:
        """Send email notifications.

        The latest run_log file (we consider latest the last one sorting
        the listing by name).

        :param exp_id: The experiment id.
        :type exp_id: str
        :param mail_to: The email address.
        :type mail_to: list[str]
        :param platform: The platform.
        :type platform: Platform
        """
        _check_mail_address(mail_to)
        message_text = _generate_message_experiment_status(exp_id, platform)
        message = MIMEMultipart()
        message['From'] = email.utils.formataddr(
            ('Autosubmit', self.config.MAIL_FROM))
        message['Subject'] = '[Autosubmit] Warning: a remote platform is malfunctioning'
        message['Date'] = email.utils.formatdate(localtime=True)
        message.attach(MIMEText(message_text))

        run_log_files = [f for f in self.config.expid_aslog_dir(
            exp_id).glob('*_run.log') if Path(f).is_file()]
        if run_log_files:
            latest_run_log: Path = max(run_log_files)
            temp_dir = TemporaryDirectory()
            try:
                compressed_run_log = _compress_file(temp_dir, latest_run_log)
                _attach_file(compressed_run_log, message)
            except AutosubmitError as e:
                Log.printlog(code=e.code, message=e.message)
            finally:
                if temp_dir:
                    temp_dir.cleanup()

        self._send_message(mail_to, self.config.MAIL_FROM, message)

    def notify_status_change(
            self,
            exp_id: str,
            job_name: str,
            prev_status: str,
            status: str,
            mail_to: list[str]) -> None:

        _check_mail_address(mail_to)
        message_text = _generate_message_text(
            exp_id, job_name, prev_status, status)
        message = MIMEText(message_text)
        message['From'] = email.utils.formataddr(
            ('Autosubmit', self.config.MAIL_FROM))
        message['Subject'] = f'[Autosubmit] The job {job_name} status has changed to {status}'
        message['Date'] = email.utils.formatdate(localtime=True)

        self._send_message(mail_to, self.config.MAIL_FROM, message)

    def _send_message(self, mail_to: list[str], mail_from: str, message) -> None:
        formatted_addresses = [
                email.utils.formataddr(
                    (mail, mail)) for mail in mail_to]
        message["To"] = ", ".join(formatted_addresses)
        try:
            self._send_mail(
                    mail_from,
                    formatted_addresses,
                    message)
        except Exception as e:
            Log.printlog(
                    f'Trace:{str(e)}\nAn error has occurred while sending a warning mail '
                    f'about remote_platform', 6011)

    def _send_mail(self, mail_from, mail_to, message):
        server = smtplib.SMTP(self.config.SMTP_SERVER, timeout=60)
        server.sendmail(mail_from, mail_to, message.as_string())
        server.quit()
